<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    
    
    
    
    
        
    
    <channel>
        <title>Posts on Luke Salamone&#39;s Blog</title>
        <link>https://blog.lukesalamone.com/posts/</link>
        <description>Recent content in Posts on Luke Salamone&#39;s Blog</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Sun, 11 Oct 2026 18:06:06 -0700</lastBuildDate>
        <atom:link href="https://blog.lukesalamone.com/posts/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>Training a Chess Reasoning Agent</title>
            <link>https://blog.lukesalamone.com/posts/chess-reasoning-llm/</link>
            <pubDate>Sun, 11 Oct 2026 18:06:06 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/chess-reasoning-llm/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TLDR: I trained an LLM reasoning agent to play chess, with model sizes 0.8B, 2B and 9B. The agent calls tools which help it keep track of the board state, which enables good performance with a relatively small number of parameters.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The demo code is &lt;a href=&#34;https://github.com/lukesalamone/chess_reasoner_demo&#34;&gt;available on Github&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;video width=&#34;760&#34; height=&#34;430&#34; style=&#34;width: 100%; max-width: 760px; height: auto;&#34; autoplay muted loop playsinline&gt;
&lt;source src=&#34;../video/chess_reasoner_0_8b.mp4&#34; type=&#34;video/mp4&#34;&gt;
Your browser does not support the video tag.
&lt;/video&gt;
&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I became excited about the power of RLVR after the &lt;a href=&#34;../posts/notes-on-deepseek-r1/&#34;&gt;Deepseek R1 paper&lt;/a&gt; came out. They demonstrated that through the use of &amp;ldquo;thinking&amp;rdquo; tokens, an LLM could solve verifiable tasks like math problems. Additionally, they described the necessity of bootstrapping the thinking process with supervised examples, which prevents the model from generating a nonsense chain of thought.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><strong>TLDR: I trained an LLM reasoning agent to play chess, with model sizes 0.8B, 2B and 9B. The agent calls tools which help it keep track of the board state, which enables good performance with a relatively small number of parameters.</strong></p>
<p><strong>The demo code is <a href="https://github.com/lukesalamone/chess_reasoner_demo">available on Github</a>.</strong></p>
<video width="760" height="430" style="width: 100%; max-width: 760px; height: auto;" autoplay muted loop playsinline>
<source src="/video/chess_reasoner_0_8b.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<h2 id="background">Background</h2>
<p>I became excited about the power of RLVR after the <a href="/posts/notes-on-deepseek-r1/">Deepseek R1 paper</a> came out. They demonstrated that through the use of &ldquo;thinking&rdquo; tokens, an LLM could solve verifiable tasks like math problems. Additionally, they described the necessity of bootstrapping the thinking process with supervised examples, which prevents the model from generating a nonsense chain of thought.</p>
<p>Chess is an ideal candidate for a verifiable rewards setup. You could call it &ldquo;fractally verifiable&rdquo;: Not only are games themselves verifiable outcomes, but the strength of individual moves is verifiable, and even the individual claims used to justify those moves can be verified.</p>
<p>The majority of prior work in chess engines has had the sole purpose of maximizing playing strength. I&rsquo;ve written a longer history of chess engines <a href="/posts/chess-engine-history/">here</a> and <a href="/posts/qualitative-analysis-chess/">here</a>. This would be a much different type of engine: we should favor explainable, intuitive moves over the absolute best moves.</p>
<h2 id="training-procedure">Training procedure</h2>
<p>The general procedure for training a reasoning model is Supervised Fine-Tuning (SFT) followed by Reinforcement Learning (RL). In my case, I used GRPO with <a href="/posts/matthew-effect/">Never Give Up</a> for the RL stage.</p>
<p>In the SFT stage, I had originally been using a LoRA with rank 16, but I found that using rank 64 was actually much better. (Similar to the <a href="https://thinkingmachines.ai/blog/lora/">LoRA Without Regret</a> paper, however, I did not see similar benefits in RL, so I kept LoRA rank at 16 there.) I also found that the SFT stage is very data-hungry, and training for one epoch over 30k samples was much better than three epochs over 10k samples.</p>
<h3 id="the-joys-of-reinforcement-learning">The Joys of Reinforcement Learning</h3>
<p>I spent a lot of time getting GRPO wrong before I got it right. Originally I used a reward proportional to the ratio in <a href="/posts/chess-blunders/">win probability</a> between the chosen move and the best move <code>win_prob(chosen) / win_prob(best_move)</code>. This made some sense since it would punish blunders and reward good moves. However, this prevented the model from caring about the quality of its moves in positions which were already won or lost. For example, a large mistake which reduced <code>win_prob</code> from 99% to 80% received a reward of around 0.8. A small inaccuracy which drops <code>win_prob</code> from 0.5 to 0.4 in a drawn position would get the same credit assignment.</p>
<p>After that, I briefly tried to use the difference <code>max(0, 1 + win_prob(chosen) - win_prob(best))</code> which is closer to how Lichess scores inaccuracies, mistakes and blunders, but this formulation had a similar issue.</p>
<p>Since win probability wasn&rsquo;t getting anywhere, I moved to a new reward simply based on CPL, which I thought would help to sharpen the difference between good moves and best moves: <code>e^(-CPL/temperature)</code> with <code>temperature=100</code> determined empirically from the CPL distribution of legal moves. With this new reward, a move with CPL 50 received a reward of 0.61, a move with CPL 100 received a reward of 0.37, and CPL of 200 received only 0.14.</p>
<h3 id="dynamics-of-grpo">Dynamics of GRPO</h3>
<p>GRPO itself has interesting behavior, which we can see from the hit@1 and hit@4 metrics, which indicate how often the model&rsquo;s move was a hit on the first attempt and any of four attempts, respectively. Typical training behavior is for GRPO to cause dramatic improvements in hit@1 vs SFT, but for hit@4 to be close to the same at the beginning and end of GRPO.</p>
<p>During the entire RL run, we are fighting the model&rsquo;s natural &ldquo;cooling and hardening&rdquo; process, known as entropy collapse. As training progresses, the diversity of moves that the model makes decreases. This is desirable on positions where the model is making good moves, since it will be less likely to make bad moves. However, it also makes it more difficult for the model to find any good move if it hasn&rsquo;t already.</p>
<p>With Never Give Up, we resample failed positions, which has the effect of dynamically scaling compute, giving the model more opportunities to find the best move. GRPO will reward samples which score better than the average reward of the group, but as training progresses and the model cools, positions which are unsuccessful are more likely to remain unsuccessful. To counteract this, we can raise the sampling temperature and apply <a href="https://arxiv.org/pdf/2503.14476">asymmetric importance sampling clipping</a>.</p>
<h2 id="why-train-an-agent">Why train an agent?</h2>
<p>The short answer is, I initially wanted to train a pure LLM, but a 4090 limits candidate base models to around 3 billion parameters. Unfortunately, the smaller models were plagued by confabulation. Reacting to phantom threats, making illegal moves, and just a general lack of grounding in reality. Allowing the model to call tools that track the board state independently removes this type of error.</p>
<h2 id="do-tools-help">Do tools help?</h2>
<p>Training the model at different sizes, we can see that tools are a very significant contributor to the model strength. Note that the numbers below are after SFT only, and GRPO typically adds 12 more points after that.</p>
<figure><img src="/img/chess_tool_scaling.png"
			alt="Skill after SFT by model size. No tools: 0.8B 6.2%, 2B 7.9%, 4B 9.6%. Tools: 0.8B 30.4%, 2B 31.8%, 4B 34.1%."><figcaption>
			<p>While model size gives modest improvement in skill, the availability of tools is a far larger factor.</p>
		</figcaption>
</figure>

<h2 id="what-tools-does-it-have">What tools does it have?</h2>
<p>The goal of these tools is to allow the harness to track the board state while the LLM reasons about how to search. They are intended to be simple and atomic operations that a human chess player can easily follow along with. None of these tools are intended to provide the LLM with judgement calls about the position.</p>
<table>
	<thead>
			<tr>
					<th>Tool Call</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>piece_on(square)</code></td>
					<td>What piece is on a square</td>
			</tr>
			<tr>
					<td><code>pieces()</code></td>
					<td>Every piece on the board and where it is (both sides)</td>
			</tr>
			<tr>
					<td><code>material()</code></td>
					<td>Material count for both sides in pawn units</td>
			</tr>
			<tr>
					<td><code>attackers(square)</code></td>
					<td>Which pieces (any side) attack a square. If occupied, distinguishes attackers from defenders</td>
			</tr>
			<tr>
					<td><code>targets(square)</code></td>
					<td>What enemy pieces the piece on square attacks, and what friendly pieces it defends</td>
			</tr>
			<tr>
					<td><code>hanging(color)</code></td>
					<td>What pieces of this color are hanging</td>
			</tr>
			<tr>
					<td><code>pins(color)</code></td>
					<td>What pins against the king for color</td>
			</tr>
			<tr>
					<td><code>forcing_moves()</code></td>
					<td>Legal checks, captures, and promotions are available in this position</td>
			</tr>
			<tr>
					<td><code>legal_moves()</code></td>
					<td>Every legal move in this position, plus whether the side to move is in check</td>
			</tr>
			<tr>
					<td><code>try(move)</code></td>
					<td>Push a move onto the stack to evaluate one ply deeper</td>
			</tr>
			<tr>
					<td><code>undo()</code></td>
					<td>Pop off the stack to back out of a search</td>
			</tr>
			<tr>
					<td><code>quiet_moves()</code></td>
					<td>Legal moves that are not in <code>forcing_moves()</code></td>
			</tr>
			<tr>
					<td><code>root()</code></td>
					<td>Pop the whole stack to return to the starting position</td>
			</tr>
			<tr>
					<td><code>threats()</code></td>
					<td>What the opponent could do if it were their move: mates, checks, captures that win material</td>
			</tr>
			<tr>
					<td><code>exchange(square)</code></td>
					<td>Result of the capture sequence on a square, starting with the best first capture</td>
			</tr>
	</tbody>
</table>
<p>When calling a tool, the LLM must also provide a justification for why it wants to call this tool. This prevents the reasoning trace from devolving into a list of bare tool calls.</p>
<h2 id="some-terminology">Some terminology</h2>
<p>Here are some terms I use throughout the post.</p>
<ul>
<li>Hit: a move which is within 50 centipawns of the best move (i.e. CPL &lt;= 50)</li>
<li>Hit rate: percentage of moves which are hits</li>
<li>Hit@k: at least one of k rollouts results in a hit i.e. a hit@4 means there was a hit after four attempts.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Paper Summary: The Matthew Effect in RL</title>
            <link>https://blog.lukesalamone.com/posts/matthew-effect/</link>
            <pubDate>Sat, 26 Sep 2026 02:36:55 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/matthew-effect/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://arxiv.org/pdf/2609.13443v1&#34;&gt;Learning to Solve Hard Problems in RL for LLMs by Never Giving Up&lt;/a&gt; discusses an approach for countering what the authors call the &lt;em&gt;Matthew Effect&lt;/em&gt;, the tendency for the LLM to improve much more on easy problems that it is already good than harder problems that have a lower solve rate. Their approach is to allocate training time dynamically based on the difficulty of the problem.&lt;/p&gt;
&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;After an LLM has been pretrained on a large corpus of supervised fine-tuning (SFT) data, it is common to post-train &lt;a href=&#34;../posts/notes-on-deepseek-r1/&#34;&gt;using reinforcement learning methods like GRPO&lt;/a&gt;. This allows the model to improve on tasks with verifiable rewards and even exceed the performance of the original SFT data.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><a href="https://arxiv.org/pdf/2609.13443v1">Learning to Solve Hard Problems in RL for LLMs by Never Giving Up</a> discusses an approach for countering what the authors call the <em>Matthew Effect</em>, the tendency for the LLM to improve much more on easy problems that it is already good than harder problems that have a lower solve rate. Their approach is to allocate training time dynamically based on the difficulty of the problem.</p>
<h2 id="background">Background</h2>
<p>After an LLM has been pretrained on a large corpus of supervised fine-tuning (SFT) data, it is common to post-train <a href="/posts/notes-on-deepseek-r1/">using reinforcement learning methods like GRPO</a>. This allows the model to improve on tasks with verifiable rewards and even exceed the performance of the original SFT data.</p>
<p>In GRPO, we compute K completions (&ldquo;rollouts&rdquo;) for a single prompt. Next, the advantage of each rollout is calculated relative to the average of its peers.</p>
<p>So at K=8,</p>
<ul>
<li>If seven of eight rollouts received a reward, the average reward of the group is 0.875. The relative reward of each correct rollout is <code>1 - avg_reward = 1.0 - 0.875 = 0.125</code>. The rollout which failed receives <code>0 - 0.875 = -0.875</code> reward.</li>
<li>If only one of the 8 receives a reward, the average reward is 0.125. The relative reward of the correct sample is <code>1 - 0.125 = 0.875</code>. Seven of eight rollouts receive <code>0 - 0.125 = -0.125</code> reward.</li>
<li>If none of the 8 samples receive a reward, the average reward is zero and all eight rollouts receive zero reward.</li>
</ul>
<p><em>Note that in reality we also normalize rewards by the standard deviation, but that detail doesn&rsquo;t change the conclusion.</em></p>
<p>Performing vanilla GRPO in this way has a problem. For easy tasks, the model will be more likely to succeed and so at least one rollout in the group will receive a reward, so all members of the group will receive a gradient. For more difficult tasks, it is possible that none of the rollouts receive a reward, in which case the gradient will be zero for the group. As a result, the model tends to improve much more on easy problems than hard ones.</p>
<figure><img src="/img/matthew_effect.png"
			alt="RL improves performance of LLMs on math and coding, but those gains are not distributed equally."><figcaption>
			<p>RL improves performance of LLMs on math and coding, but those gains are not distributed equally.</p>
		</figcaption>
</figure>

<p>The authors call the phenomenon of zero gradient on difficult problems the <em>signal loss hypothesis</em>. One standard way to combat the signal loss phenonmenon is to simply increase K, which increases the chance of the group containing at least one good rollout.</p>
<p>This problem and solution seem reasonable, but the authors also suggest a related explanation called the <em>signal efficiency hypothesis</em>: in addition to having too few examples of difficult problems, the model might be seeing too many examples of good solutions to easy problems.</p>
<h2 id="probably-dont-give-up">Probably don&rsquo;t give up</h2>
<p>The authors come up with a nice solution to this problem:</p>
<ol>
<li>
<p>First, sample a small number K completions for the prompt (e.g. 4).</p>
</li>
<li>
<p>If all of the completions are correct, it&rsquo;s an easy problem and we don&rsquo;t need to train on it.</p>
</li>
<li>
<p>If all completions are wrong, we <em>probably</em> try again. The authors&rsquo; algorithm puts the problem back into the queue with probability <code>p_ngu</code>, and sample K more completions</p>
</li>
</ol>
<ul>
<li>For example with <code>p_ngu = 0.95</code> there&rsquo;s a 95% chance they try K more times, and if all of those rollouts fail there&rsquo;s a 95% chance that they try K more times.</li>
</ul>
<p>This algorithm allows us to dynamically scale compute based on problem difficulty.Easy problems might not be trained on at all, and hard ones can receive many attempts.</p>
<p>Additionally, because a rare solution to a hard problem will be compared to a large group of unsuccessful rollouts, the solution will receive a large reward <code>1-average_reward</code>.</p>
<p>Because after each batch of all-failed rollouts there is a small probability that the algorithm won&rsquo;t retry the prompt, the algorithm is more accurately called <em>probably don&rsquo;t give up</em>. However, that name may have been taken.</p>
<h2 id="results">Results</h2>
<p>The authors trained Qwen 3 4B-base on 10k problems from Deepscaler math, dividing problems into 4 difficulty buckets based on Qwen&rsquo;s initial solve rate. They then compared the performance of simply increasing K across all problems with using <code>p_ngu</code> of 0.5, 0.75, and 0.875.</p>
<p>They found that simply increasing K on all problems sacrifices performance on easy problems, while NGU was able to maintain performance on easy problems while improving even more on difficult ones.</p>
<p>This is likely because if performance begins to degrade on an easy problem, the algorithm can dynamically allocate more compute and put it back into the queue as well. In contrast, simply allocating K rollouts at the beginning of training based on initial performance fails to take into account fluctuations in performance during training.</p>
<figure><img src="/img/ngu_vs_increasing_k.png"
			alt="On hard problems, NGU outperforms the strategy of simply increasing K, and on easy problems it matches performance."><figcaption>
			<p>On hard problems, NGU outperforms the strategy of simply increasing K, and on easy problems it matches performance.</p>
		</figcaption>
</figure>

<p>On a coding-like benchmark, asking the model to play a game called Manufactoria, NGU is able to dynamically allocate compute towards the harder test cases, significantly outperforming vanilla GRPO.</p>
<figure><img src="/img/manufactoria_ngu_vs_grpo.png"
			alt="Vanilla GRPO struggles with hard tests, meaning it cannot solve even a single example in the game."><figcaption>
			<p>Vanilla GRPO struggles with hard tests, meaning it cannot solve even a single example in the game.</p>
		</figcaption>
</figure>

]]></content>
        </item>
        
        <item>
            <title>Semantic Search in Under 3MB</title>
            <link>https://blog.lukesalamone.com/posts/creating-tiny-semantic-search/</link>
            <pubDate>Fri, 19 Jun 2026 22:24:12 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/creating-tiny-semantic-search/</guid>
            <description>&lt;p&gt;This project is a continuation of my &lt;a href=&#34;../posts/autoresearch/&#34;&gt;previous autoresearch project&lt;/a&gt; which was a continuation of &lt;a href=&#34;../posts/rolling-my-own-blog-search/&#34;&gt;a previous client-side search project&lt;/a&gt;. The autoresearch project optimized a reranking model to be under 10MB. Digging deeper by hand, I was able to take the size reduction much further, while outperforming reranking models which are 30x larger on this task. In the end I was able to reduce the payload from 11.4 MB to 2.79 MB gzipped.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>This project is a continuation of my <a href="/posts/autoresearch/">previous autoresearch project</a> which was a continuation of <a href="/posts/rolling-my-own-blog-search/">a previous client-side search project</a>. The autoresearch project optimized a reranking model to be under 10MB. Digging deeper by hand, I was able to take the size reduction much further, while outperforming reranking models which are 30x larger on this task. In the end I was able to reduce the payload from 11.4 MB to 2.79 MB gzipped.</p>
<p>You can see it in action on my <a href="https://lukesalamone.com">resume page</a>.</p>
<figure><img src="/img/tiny_reranker.png"
			alt="Each square represents 1 kB. The majority of overall size reduction came from removing the ORT dependency. However, other changes enabled much better representation quality than the baseline."><figcaption>
			<p>Each square represents 1 kB. The majority of overall size reduction came from removing the ORT dependency. However, other changes enabled much better representation quality than the baseline.</p>
		</figcaption>
</figure>

<h2 id="baseline">Baseline</h2>
<p>After running my <a href="/posts/autoresearch/">original autoresearch experiment</a> overnight, I had fairly impressive but tiny 4.3M param dual encoder, quantized to int8 onnx.</p>
<p>However, when I tried using it in my <a href="https://lukesalamone.com">resume page</a>, it wasn&rsquo;t actually that good. In fact, it was much worse than BM25 that was working alongside it. It is notable that on the eval set I used, much larger models like <code>all-MiniLM-L6-v2</code> also didn&rsquo;t perform very well. This suggested that the issue was in training and domain adaptation, not size.</p>
<h2 id="term-dropout">Term dropout</h2>
<p>One of the motivating issues was that the model failing to correctly rank docs under simple queries like <em>do you have any leadership experience</em>. The model was latching onto terms like &ldquo;Grammarly&rdquo; but not to more abstract ones like &ldquo;leadership&rdquo;.</p>
<p>To address this, I performed term dropout, randomly dropping the top TF-IDF term from queries and docs with 20% probability. This both normalized the model against overfitting and helped to combat simple keyword matching that BM25 would already be doing.</p>
<p>Because the corpus is so small, we need to prevent overfitting.</p>
<h2 id="query-mining-from-job-postings">Query mining from job postings</h2>
<p>To increase the diversity and realism of potential queries to the model, I created a small pipeline to pull queries from real job postings:</p>
<ol>
<li>First, we gather relevant job postings from <a href="https://kaleh.net/trace/">Kaleh job postings api</a>, which has a generous free tier.</li>
<li>Next, pull out &ldquo;queries&rdquo; from the job postings using a local LLM (<code>phi4</code>)
<ul>
<li>A &ldquo;query&rdquo; amounts to a rephrasing of a job qualification e.g. &ldquo;experience with python&rdquo; -&gt; &ldquo;do you have experience with python&rdquo;</li>
</ul>
</li>
<li>Finally, use the same LLM to identify high-precision matches from my actual resume, if any.</li>
</ol>
<p>After one round of this process, I had mined 94 queries which raised MRR 21%. But after a second round with ~1000 pairs MRR had mostly plateaued, increasing the same metric ~3 points.</p>
<h2 id="architecture-ablations">Architecture ablations</h2>
<p>At this point, the model was pretty decent for its size. I wanted to see how much more performance we could squeeze out of it, so I ran a series of quick architecture experiments:</p>
<p><strong>Max pooling vs mean pooling</strong>: In order to get the final 256-dimensional embedding for comparison, we need to convert the L x 256 output matrix from the model into a 256-dimensional vector, where L is up to 64 tokens long. Previously we used the mean of each column, which is known as &ldquo;mean pooling&rdquo;. Switching to max pooling was slightly better (0.60 to 0.634 on a harder query set) because it allowed the strongest value to directly affect the output.</p>
<p><strong>Factorized embeddings</strong>: Embeddings for each token are stored in a lookup table with shape (V x D). However, we can factorize this matrix using a low-rank approximation, <a href="https://arxiv.org/abs/1909.11942">which is what ALBERT does to save parameters</a>. This saves ~1M params and was neutral on nDCG.</p>
<p><strong>SwiGLU</strong>: SwiGLU replaces GELU with a gated unit, and can theoretically lead to richer intermediate representations. However, SwiGLU resulted in no measurable performance gain.</p>
<p><strong>Multi-vector late interaction</strong>: Rather than pooling output vectors with mean/max pooling, we use ColBERT-style token-level expressiveness using the MaxSim function. Concretely it looks like this:</p>
<pre><code class="language-python">def maxsim(query:str, document:str) -&gt; float:
    query_vectors = encoder(query)
    document_vectors = encoder(document)

    score = 0
    for q in query_vectors:
        # find the highest dot-product match among all
        # document token vectors
        best = max(np.dot(q,d) for d in document_vectors)
        score += best
    return score
</code></pre>
<h2 id="vocab-pruning">Vocab pruning</h2>
<p>The first step after tokenizing a string is to get each token’s corresponding embedding from a lookup table. That means the model stores <code>vocab_size x embedding_dimension</code> parameters which is ~8MB already. To make this smaller on disk there’s really only three things you can do: reduce the vocab, reduce the embedding dimension, and reduce bytes per param (quantization). I did all three.</p>
<p>The full BERT wordpiece vocab is 30k tokens, which is way too much. It contains ~1000 “unused” tokens (tokens like <code>[unused123]</code>, <code>[unused456]</code>), tokens in foreign languages, and full words like “baltimore” and “vampires” that are irrelevant to my resume. I was able to cut it down to 5000 tokens safely by trimming “unused” and foreign language tokens, then whitelisted tokens present in documents.</p>
<p>Second, I reduced the embedding dimension</p>
<ul>
<li>I directly reduced the embedding dimension from 768 to 256.</li>
<li>I also tried to reduce the effective dimension even further by factorizing the embedding table, but this ended up causing a quality regression and was reverted.</li>
</ul>
<p>I also aggressively quantized the model, which enabled savings for the whole model, not just the embedding table.</p>
<h2 id="replacing-onnxruntime-web">Replacing onnxruntime-web</h2>
<p>After pruning the vocab, I realized another large source of file size overhead was the inference code itself, <a href="https://onnxruntime.ai/docs/tutorials/web/">ONNX Runtime Web</a>. ORT was 3.4MB over the wire, 13.2MB uncompressed. The reason it&rsquo;s so big is that ORT is a general-purpose interpreter for ONNX files, which contain a graph of the weights and all operations in inference.</p>
<p>But my model is extremely simple, and only contains a few different operations. So instead of shipping the model as an <code>.onnx</code> file and relying on ORT to handle it, I shipped the model as a <code>.safetensors</code> file and did the interpretation in a custom WASM binary, built from Rust. To prevent any issues, I ensured numerical parity with ORT prior to swapping it out.</p>
<p>Dropping ONNX means we don&rsquo;t need general-purpose functions like <code>Transpose</code>, <code>Concat</code>, <code>Reshape</code>, etc. We can get away with index arithmatic alone. That said, I didn&rsquo;t implement any multithreading or SIMD since the model is so small.</p>
<p><a href="https://huggingface.co/docs/safetensors/index">Safetensors</a> is a standard protocol for shipping files and only contains three parts:</p>
<ol>
<li>8 bytes specifying header size</li>
<li>The header, containing byte offsets into the payload</li>
<li>The payload, containing raw bytes</li>
</ol>
<p>Note that technically I don&rsquo;t even need the first two parts of the safetensors file at all for this, but I felt like shipping in a standard format was worth the small size tradeoff here.</p>
<p><strong>In the end, this change reduced the cost of the inference logic over the wire from 3.4 MB to 4 kB, an 850x reduction.</strong></p>
<h2 id="158-bit-quantization">1.58-bit quantization</h2>
<p>Quantization is a pretty standard technique for model size reduction. Models which are trained at 32 bit precision can be served at 16, 8, or even 4 bits while maintaining acceptable quality. <a href="https://en.wikipedia.org/wiki/1.58-bit_large_language_model">1.58 bit quantization</a> is the most aggressive quantization which tends to work. Ternary quantization restricts weights to only three values: <code>{-1, 0, +1}</code>.</p>
<p>Since this task is a simple reranking task, quality degradation is less obvious than if we were quantizing a decoder model, since there is no &ldquo;cascade&rdquo; of increasingly incorrect tokens that will be generated. For this task, I used self-distillation, with the teacher being the multi-vector late interaction model from before, training directly into ternary.</p>
<p>Note that I only quantized the weights to 1.58 bit, and kept the activations in f16. This helped to cut down on the errors and has no effect on the cost over the wire.</p>
<p><strong>Overall, 1.58 bit quantization resulted in an over-the-wire file size reduction from 8.3MB to 3.9MB</strong>.</p>
<h2 id="results">Results</h2>
<p>Graded nDCG@10 over 43 LLM-judged queries (0–3 relevance), with a 21-query hard subset:</p>
<table>
	<thead>
			<tr>
					<th>Ranker</th>
					<th>nDCG@10 (all)</th>
					<th>nDCG@10 (hard)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>auto_researcher distilled model (baseline)</td>
					<td>0.562</td>
					<td>0.285</td>
			</tr>
			<tr>
					<td>BM25 lexical baseline</td>
					<td>0.641</td>
					<td>0.507</td>
			</tr>
			<tr>
					<td>shipped 1.58-bit model (after)</td>
					<td>0.787</td>
					<td>0.694</td>
			</tr>
	</tbody>
</table>
<h2 id="other-experiments">Other experiments</h2>
<p>Some of the things I tried didn&rsquo;t work, either providing no quality benefit or helping on file size while damaging quality too much:</p>
<p><strong>The original distilled embedding model</strong>. The 4.7 MB ONNX model from autoresearch wasn&rsquo;t finetuned for this task. It actually performed worse than BM25 on this corpus. It&rsquo;s what motivated this entire improvement process. I believe that smaller models have much more difficulty generalizing, although they can still be finetuned for very specific tasks.</p>
<p><strong>Factorizing token embeddings after training</strong>. Factorizing the table after training with a low-rank SVD caused high reconstruction error and visibly hurt ranking for ~1 MB saved. The same factorization applied during training was neutral.</p>
<p><strong>Attention pooling</strong>. I tried added a pooling head but it had no benefit over max-pooling and required additional parameters.</p>
<p><strong>SwiGLU FFN</strong>. I tried replacing GELU with SwiGLU, but it didn&rsquo;t improve the model&rsquo;s quality. I believe it might have been more helpful in making sense out of more complex representations, but this model is fairly straightforward.</p>
<p><strong>Ternary cross-encoder</strong>. This was my original attempted ternary quantization. As a cross encoder, it actually matched the quality of the much larger dual encoder teacher. <a href="https://proceedings.mlr.press/v162/menon22a/menon22a.pdf">Cross encoders are known to be more expressive than dual encoders</a>, so if this didn&rsquo;t work it would mean ternary quantization was too strong for this task.</p>
<p><strong>Diminishing returns of extra data</strong>. I scaled the job post mining from 10 postings to 260, but there were significant diminishing returns. I believe this is partially due to the diversity of queries extracted from the job postings, so it might still be possible to increase diversity somewhat without wasting parameters on out-of-domain concepts. At some point, I don&rsquo;t really care if my model knows what <a href="https://en.wikipedia.org/wiki/Antidisestablishmentarianism">antidisestablishmentarianism</a> is.</p>
<h2 id="extending-techniques-to-blog-search">Extending techniques to Blog Search</h2>
<p>Next, I applied many of the same techniques to the search on this blog, with some slight adaptations.</p>
<p>First, because this is a blog with a much larger scope of topics, I needed a larger vocabulary. I pruned the vocab in the following way:</p>
<ol>
<li>All tokens contained in docs are kept (protected).</li>
<li>The rest of the tokens (non-protected) are ranked by their max cosine sim with any blog post</li>
<li>I generated a small set of synthetic queries and had Qwen rate each blog post 0-3 for relevance against each query</li>
<li>Among the remaining (non-protected) tokens I ran an nDCG sweep over number of tokens to prune, removing the least relevant N tokens in each trial.</li>
</ol>
<p>After this process, I still had ~13k tokens in the vocab. This meant that I actually needed to use int4 precision to store token embeddings, which is even lower than before.</p>
<p>Second, pooling strategy. <code>MaxSim</code> is very powerful, but it runs a similarity against every query token and every doc token. This is computationally infeasible. Instead, the blog encoder uses simple mean pooling. The embeddings from the blog correspond to larger chunks of text which have more semantic meaning, so the quality tradeoff is acceptable.</p>
<p>Since blog text is a much less specialized domain than resume text, I also opted to simply distill from <code>all-MiniLM-L6-v2</code> as a teacher, which is a more standard reranking model. This was standard KL distillation from the full-precision teacher into a ternary student, and uses the same number and sizes of layers as the teacher.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I learned a few things here. First, small models can work in narrow domains, and even outperform much larger models.</p>
<p>Second, having an eval set we can continuously test ideas against helps tremendously. Some things, like adding an attention pooling head, seem like they should work. But when they are clearly outclassed by things like late interaction with MaxSim, it allows us to move on to optimizing other parts of the model.</p>
<p>Third, although none of these tricks are particularly exotic, it&rsquo;s good to have a sense of what works and what doesn&rsquo;t. Late interaction vectors really work, although they have a cost tradeoff. Encoders ship with a ton of tokens which aren&rsquo;t used. Ternary quantization really works, although the errors probably become more pronounced as the domain expands.</p>
]]></content>
        </item>
        
        <item>
            <title>Teaching a Neural Net to Fight</title>
            <link>https://blog.lukesalamone.com/posts/fighting-game-rl/</link>
            <pubDate>Mon, 08 Jun 2026 00:20:43 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/fighting-game-rl/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;p&gt;In this project, I wanted to set up what I thought was a simple experiment: if we let a neural network play a fighting game, will it learn any interesting emergent behaviors? I would say the answer is a resounding &amp;ldquo;kind of&amp;rdquo;. The players will find &amp;ldquo;optimal&amp;rdquo; strategies, but whether we see those optimal strategies as interesting or simply as hacks is a bit of a matter of opinion.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<p>In this project, I wanted to set up what I thought was a simple experiment: if we let a neural network play a fighting game, will it learn any interesting emergent behaviors? I would say the answer is a resounding &ldquo;kind of&rdquo;. The players will find &ldquo;optimal&rdquo; strategies, but whether we see those optimal strategies as interesting or simply as hacks is a bit of a matter of opinion.</p>
<p><strong>Introducing Neural Knockout!</strong> Below, you can try your hand playing against the agent trained via opponent league.</p>
<div style="max-width:760px;margin:0 auto;aspect-ratio:16/9"><iframe src="/streetfighter-demo/" style="width:100%;height:100%;border:1px solid #444" allow="keyboard" loading="lazy"></iframe></div>
<p><em>Credit to <a href="https://luizmelo.itch.io/">Luiz Melo</a> for game assets.</em></p>
<h2 id="technical-background">Technical background</h2>
<p>Reinforcement learning is commonly used in tasks like games (and more recently, language modeling) where we want the model to take a long sequence of actions, and we want to label the entire sequence as good or bad. In a game, the action might be to take one step to the right. In language modeling, the action might be to emit a single token.</p>
<p>Unlike in regular supervised learning, in reinforcement learning we don&rsquo;t know whether an individual action is good or bad. We will wait until the game ends or the writing sample is complete, and then score the entire sequence.</p>
<p>And what if the sequence never ends? Welcome to the wonderful world of <em>the cold start problem</em>.</p>
<h2 id="reward-design">Reward design</h2>
<p>The simplest version of rewards in RL is to give +1 to the winning model and -1 to the losing model. In 2D fighting games like Streetfighter and Mortal Kombat the players start on opposite sides of the screen. To a human, it&rsquo;s pretty obvious what to do next. But for an RL agent initialized with a random policy, going to the other side of the screen and actually attacking the opponent until you win is a small miracle.</p>
<details>
    <summary>🍿Click here to view</summary>
    <video width="760" height="570" controls>
      <source src="/img/neural_knockout/m006_v24_early_5M.mp4" type="video/mp4">
    Your browser does not support the video tag.
    </video>
</details>
<p>The problem above is actually two separate issues. First, the players don&rsquo;t approach each other. They aren&rsquo;t shy, they just don&rsquo;t know any better. Second, even when they do, they don&rsquo;t know to attack the opponent. They will jump or walk away and it will take a very long time to actually win or lose. And those two problems result in a third problem of credit assignment. It&rsquo;s really hard to reward the useful actions if they&rsquo;re buried in a pile of irrelevant ones.</p>
<p>To fix these problems, we can use reward shaping, which encourages the agents to take actions we&rsquo;d like them to by being more precise about how we reward the model. To fix the distance issue, we can give a small reward for closing the distance with the opponent. To fix the pacifism issue, we can reward the models with <code>opponent_damage - player_damage</code>, so that giving the opponent damage increases the reward and taking damage reduces the reward.</p>
<h3 id="grabs">Grabs</h3>
<p>After adding those two reward shaping changes, the agents learned to approach and attack, throwing in a good amount of hopping and a bit of shielding as well. However, after watching a few of the games, one of the players seemed to learn that simply spamming attacks against the opponent&rsquo;s shield was enough to win.</p>
<details>
    <summary>🍿Click here to view</summary>
    <video width="760" height="570" controls>
      <source src="/img/neural_knockout/m026_v36_late_99M_cpu_80pct.mp4" type="video/mp4">
    Your browser does not support the video tag.
    </video>
</details>
<p>It seemed like we needed to add a grab-slam mechanic to counter this: shield + attack = grab, which could punish attacks that hit an opponent&rsquo;s shield. This rock-paper-scissors mix is commonly found in fighting games.</p>
<p>Grabbing was a slightly more complex mechanic for the model to learn since it requires landing a grab then pressing <code>attack</code> (to slam) to actually do any damage. I added a small reward for actually landing a grab in order to encourage more grabbing. After restarting the training, the model still took 88 million steps to learn to grab consistently.</p>
<figure><img src="/img/v33_meta.png"
			alt="Run 33: grab -&gt; slam was discovered around 88M steps, receding as the opponent adapted."><figcaption>
			<p>Run 33: grab -&gt; slam was discovered around 88M steps, receding as the opponent adapted.</p>
		</figcaption>
</figure>

<details>
    <summary>🍿Click here to view</summary>
    <video width="760" height="570" controls>
      <source src="/img/neural_knockout/m093_grab_v36_16M.mp4" type="video/mp4">
    Your browser does not support the video tag.
    </video>
</details>
<h2 id="hopping">Hopping</h2>
<p>Another interesting metagame appeared after a few training runs. The models found that being in the air avoided attacks entirely, and jump attacks were un-counterable.</p>
<p>This was a clear flaw in the game design, which I resolved by increasing landing lag and increasing the height of each grounded player&rsquo;s hitbox so they can hit the airborne player. RL agents are good at discovering bugs like these.</p>
<details>
    <summary>🍿Click here to view</summary>
    <video width="760" height="570" controls>
      <source src="/img/neural_knockout/m016_v32_bunny_hop_analysis_99M.mp4" type="video/mp4">
    Your browser does not support the video tag.
    </video>
</details>
<h2 id="league-play">League play</h2>
<p>Up until this point, I had been training the models using (asymmetric) self-play: both policies play a set of games against each other, then models are trained on the data from those games. One model&rsquo;s loss is another model&rsquo;s win, and the models learn to take actions to maximize their respective rewards.</p>
<p>However, self-play can lead the policies to learn brittle strategies. Rather than learning well-rounded play, the models will find that the most reliable way to get reward is to beat the particular opponent it is training against.</p>
<figure><img src="/img/selfplay_plateau.png"
			alt="After pitting all checkpoints from run 38 against each other, we would expect that later checkpoints would outperform earlier ones since they received more compute. In reality, they plateau after ~200M steps. This suggests that rather than improving together, the two players were doing something more akin to a sophisticated dance, learning to counter the other&#39;s narrow gameplay style."><figcaption>
			<p>After pitting all checkpoints from run 38 against each other, we would expect that later checkpoints would outperform earlier ones since they received more compute. In reality, they plateau after ~200M steps. This suggests that rather than improving together, the two players were doing something more akin to a sophisticated dance, learning to counter the other's narrow gameplay style.</p>
		</figcaption>
</figure>

<p>Rather than playing against a single, fixed opponent, pitting the Main policy against a pool of opponents should give it a more diverse experience. I created a league of opponents for Main composed of the following agent types (inspired by <a href="https://github.com/wenzhe-li/FightLadder">FightLadder</a>):</p>
<ul>
<li><strong>Opponent</strong>. The most recent snapshot of the model trained to play against Main, also trained against a pool.</li>
<li><strong>Exploiter</strong>. An agent trained against a frozen version of Main, specializing in beating Main (and only Main).</li>
<li><strong>League Exploiters</strong>. Trained against the pool of previous checkpoints.</li>
<li><strong>Historicals</strong>. Previous copies of Main, before it was improved.</li>
</ul>
<p>Here we use Prioritized Fictitious Self-Play, meaning that Main will face opponents that it loses to more often (specifically, $ (1-w)^2 $, where $w$ is Main&rsquo;s win rate against that opponent).</p>
<p>League training works. The model trained for 48M steps in the league won every matchup against previously-trained self-play-only checkpoints, despite never facing them. This also indicates that the strategies that those self-play agents discovered were independently discovered in the league to some respect.</p>
<figure><img src="/img/league.png"
			alt="Main&rsquo;s win rate stays around 50% even as the historical pool grows to ~66 checkpoints."><figcaption>
			<p>Main&rsquo;s win rate stays around 50% even as the historical pool grows to ~66 checkpoints.</p>
		</figcaption>
</figure>

<details>
    <summary>🍿Click here to view</summary>
    <video width="760" height="570" controls>
      <source src="/img/neural_knockout/m071_v38_vs_league_v2.mp4" type="video/mp4">
    Your browser does not support the video tag.
    </video>
</details>
<h2 id="practical-note">Practical note</h2>
<p>Originally, I was copying game logic back and forth between Python training code and the HTML game viewing code. This is tedious and error-prone.</p>
<p>Instead, it is better to write the game engine once in Rust, then import it into the browser with <code>wasm-pack</code> and into the python training environment with <code>maturin</code>. Rust is also much faster than Python, so it&rsquo;s easy to simulate billions of steps overnight if necessary.</p>
<p>See also: <a href="/posts/how-to-create-rust-python-bindings/">How to Create Rust Python Bindings</a></p>
<h2 id="applications">Applications</h2>
<p>I believe this method, co-evolving a game with agents that learn to play it, can be used to find degenerate strategies in games during development. This kind of &ldquo;reward hacking&rdquo; means that agents will find the simplest way to receive a reward.</p>
<p>Therefore, training simple agents to play a game can be used to find bugs before sending a game to playtesters. If RL agents are able to break a game with silly strategies, it might be worth rebalancing before humans see it.</p>
]]></content>
        </item>
        
        <item>
            <title>Definitions of Model</title>
            <link>https://blog.lukesalamone.com/posts/model-is-overloaded/</link>
            <pubDate>Sun, 05 Apr 2026 19:25:25 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/model-is-overloaded/</guid>
            <description>&lt;p&gt;There are a lot of meanings of the term &amp;ldquo;model&amp;rdquo; in machine learning and machine learning-adjacent fields. Depending on the context, &amp;ldquo;model&amp;rdquo; can mean:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;a general architecture/configuration for weights of a specific model&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;e.g. &amp;ldquo;We embedded texts using a BERT model&amp;rdquo; might mean DistillBERT, RoBERTa, ModernBERT etc which all differ from the architecture in &lt;a href=&#34;https://arxiv.org/pdf/1810.04805&#34;&gt;the original 2018 BERT paper&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a specific set of weights which are the result of a training process&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;e.g. &amp;ldquo;You can download the model from HuggingFace&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Confusingly, &lt;a href=&#34;https://github.com/huggingface/parler-tts/issues/175&#34;&gt;models can be versioned&lt;/a&gt; to fix bugs so the same model (by definition #1) can actually be different models (by definition #2).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a product and the system around that product, which isn&amp;rsquo;t quite #2 because it likely includes additional systems like guardrails and rate limiting. And the specific weights used might change without the end-user knowing, such as with additional fine-tuning.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;e.g. &amp;ldquo;Our newest Claude model is the most powerful yet&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;in reinforcement learning, a &amp;ldquo;model&amp;rdquo; is simply the understanding of how the world works, i.e. the &amp;ldquo;world model&amp;rdquo;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This leads to the confusing situation of &amp;ldquo;model-free&amp;rdquo; systems which still include neural networks, but simply don&amp;rsquo;t act based on any information about what effects their actions will have on the world.&lt;/li&gt;
&lt;li&gt;For example, AlphaZero is not a model-free algorithm because it uses Monte Carlo Tree Search, and each node in that tree is the result an action taken in a simulated world. It&amp;rsquo;s not learning from scratch what the resulting chessboard if it moves a piece.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;since it&amp;rsquo;s also possible to &amp;ldquo;model&amp;rdquo; a complex process with a simpler one, a &amp;ldquo;model&amp;rdquo; might be some heuristic or simpler algorithm which is implied to be imperfect.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is the original etymology behind language &amp;ldquo;modeling&amp;rdquo; since early models were simple bigrams and trigrams.&lt;/li&gt;
&lt;li&gt;This is also the connotation of climate models, economic models, and models of the spread of disease.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
            <content type="html"><![CDATA[<p>There are a lot of meanings of the term &ldquo;model&rdquo; in machine learning and machine learning-adjacent fields. Depending on the context, &ldquo;model&rdquo; can mean:</p>
<ol>
<li>
<p>a general architecture/configuration for weights of a specific model</p>
<ul>
<li>e.g. &ldquo;We embedded texts using a BERT model&rdquo; might mean DistillBERT, RoBERTa, ModernBERT etc which all differ from the architecture in <a href="https://arxiv.org/pdf/1810.04805">the original 2018 BERT paper</a>.</li>
</ul>
</li>
<li>
<p>a specific set of weights which are the result of a training process</p>
<ul>
<li>e.g. &ldquo;You can download the model from HuggingFace&rdquo;</li>
<li>Confusingly, <a href="https://github.com/huggingface/parler-tts/issues/175">models can be versioned</a> to fix bugs so the same model (by definition #1) can actually be different models (by definition #2).</li>
</ul>
</li>
<li>
<p>a product and the system around that product, which isn&rsquo;t quite #2 because it likely includes additional systems like guardrails and rate limiting. And the specific weights used might change without the end-user knowing, such as with additional fine-tuning.</p>
<ul>
<li>e.g. &ldquo;Our newest Claude model is the most powerful yet&rdquo;</li>
</ul>
</li>
<li>
<p>in reinforcement learning, a &ldquo;model&rdquo; is simply the understanding of how the world works, i.e. the &ldquo;world model&rdquo;.</p>
<ul>
<li>This leads to the confusing situation of &ldquo;model-free&rdquo; systems which still include neural networks, but simply don&rsquo;t act based on any information about what effects their actions will have on the world.</li>
<li>For example, AlphaZero is not a model-free algorithm because it uses Monte Carlo Tree Search, and each node in that tree is the result an action taken in a simulated world. It&rsquo;s not learning from scratch what the resulting chessboard if it moves a piece.</li>
</ul>
</li>
<li>
<p>since it&rsquo;s also possible to &ldquo;model&rdquo; a complex process with a simpler one, a &ldquo;model&rdquo; might be some heuristic or simpler algorithm which is implied to be imperfect.</p>
<ul>
<li>This is the original etymology behind language &ldquo;modeling&rdquo; since early models were simple bigrams and trigrams.</li>
<li>This is also the connotation of climate models, economic models, and models of the spread of disease.</li>
</ul>
</li>
</ol>
]]></content>
        </item>
        
        <item>
            <title>Autoresearch</title>
            <link>https://blog.lukesalamone.com/posts/autoresearch/</link>
            <pubDate>Wed, 11 Mar 2026 20:14:56 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/autoresearch/</guid>
            <description>&lt;figure&gt;&lt;img src=&#34;../img/autoresearch_progress.png&#34;
			alt=&#34;Leaving the autoresearch loop going, the LLM was able to make 7.8% progress on the distillation task.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;Leaving the autoresearch loop going, the LLM was able to make 7.8% progress on the distillation task.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I saw Andrej Karpathy&amp;rsquo;s &lt;a href=&#34;https://github.com/karpathy/autoresearch&#34;&gt;Autoresearch&lt;/a&gt; results the other day and decided to give it a shot on a relatively difficult task: distilling a retrieval/reranking model into a much smaller 10MB model. &lt;a href=&#34;../posts/rolling-my-own-blog-search/&#34;&gt;This is something that I had wanted to do for a few years now&lt;/a&gt;. The model was trained on MS MARCO v1.1 and SQuAD (~169K pairs total) and evaluated on MRR@10.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<figure><img src="/img/autoresearch_progress.png"
			alt="Leaving the autoresearch loop going, the LLM was able to make 7.8% progress on the distillation task."><figcaption>
			<p>Leaving the autoresearch loop going, the LLM was able to make 7.8% progress on the distillation task.</p>
		</figcaption>
</figure>

<p>I saw Andrej Karpathy&rsquo;s <a href="https://github.com/karpathy/autoresearch">Autoresearch</a> results the other day and decided to give it a shot on a relatively difficult task: distilling a retrieval/reranking model into a much smaller 10MB model. <a href="/posts/rolling-my-own-blog-search/">This is something that I had wanted to do for a few years now</a>. The model was trained on MS MARCO v1.1 and SQuAD (~169K pairs total) and evaluated on MRR@10.</p>
<h2 id="setup">Setup</h2>
<p>I opted for a fully local setup. Claude Code might have been better, but I wanted to control the costs of this experiment. Instead, I went with <code>qwen3-coder:30b</code> as the &ldquo;researcher&rdquo; and used the following scheduling loop:</p>
<ol>
<li>Researcher checks prior experiment progress and proposes of 55 minutes of experiments, writing them into a todo log.</li>
</ol>
<ul>
<li>Experiments can have variable duration. It is up to the researcher to allocate time.</li>
<li>The researcher can also update <code>train.py</code> to accomodate new experiments.</li>
<li>There is a 5 minute time limit for the researcher, but I never hit it in practice.</li>
</ul>
<ol start="2">
<li>Scheduler reads from todo log. Each experiment must have a duration, which is a pencils-down hard cutoff time</li>
<li>Scheduler runs experiments from the todo log, recording results to results.txt</li>
<li>Go back to step 1</li>
</ol>
<h2 id="results">Results</h2>
<p>Although the researcher was able to find promising results, most of the things it proposed were pretty basic hyperparameter optimizations. While these are important,</p>
<ul>
<li>I was really hoping for more novelty and creativity, but it only updated <code>train.py</code> one time, to implement <code>MarginMSELoss</code>.</li>
<li>If it is just going to optimize hyperparameters, Bayesian Optimization is a much faster and more effective ways to do that.</li>
</ul>
<p>That said, the researcher isn&rsquo;t just doing a random walk of the problem space. There is a clear pattern of improvement even among the failed experiments.</p>
]]></content>
        </item>
        
        <item>
            <title>Distilling Stockfish with One Billion Positions</title>
            <link>https://blog.lukesalamone.com/posts/distilling-stockfish/</link>
            <pubDate>Fri, 06 Mar 2026 20:25:56 -0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/distilling-stockfish/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TLDR: I extracted fens and stockfish evaluations for 3.9 billion chess positions. I then trained a neural network on 1 billion of them. To my knowledge, this is the largest open state-value chess dataset released. The dataset is released as the &lt;a href=&#34;https://huggingface.co/datasets/lukesalamone/gigafish-3.8b-d10&#34;&gt;Gigafish dataset on Huggingface&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;../img/distill_stockfish_loss_curve.png&#34;
			alt=&#34;Loss continued to decrease during the entire training run, strongly suggesting the importance of data volume.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;Loss continued to decrease during the entire training run, strongly suggesting the importance of data volume.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;</description>
            <content type="html"><![CDATA[<p><strong>TLDR: I extracted fens and stockfish evaluations for 3.9 billion chess positions. I then trained a neural network on 1 billion of them. To my knowledge, this is the largest open state-value chess dataset released. The dataset is released as the <a href="https://huggingface.co/datasets/lukesalamone/gigafish-3.8b-d10">Gigafish dataset on Huggingface</a>.</strong></p>
<figure><img src="/img/distill_stockfish_loss_curve.png"
			alt="Loss continued to decrease during the entire training run, strongly suggesting the importance of data volume."><figcaption>
			<p>Loss continued to decrease during the entire training run, strongly suggesting the importance of data volume.</p>
		</figcaption>
</figure>

<p>Stockfish is currently the strongest chess engine in the world. It works by using a small neural network millions of times per position. One interesting question we could ask is whether we could replace search depth with a more accurate value function. In 2024, researchers at DeepMind <a href="https://arxiv.org/pdf/2402.04494v1">attempted</a> to answer this question. Critically, this approach did not hold search depth constant, and although they had 15 billion action-value pairs, they only had around 530 million state-value estimates.</p>
<p>The stockfish evaluation of a position is <a href="/posts/chess-blunders/">a good indication of win probability</a>, so I wanted to take another look at this problem. To do this, I built a large state-value dataset with a fixed search depth and experimented with different model architectures.</p>
<h2 id="attention-isnt-all-you-need">Attention isn&rsquo;t all you need</h2>
<p>I trained variants from three main categories of architectures: pure Vision Transformer, pure ResNet, and a hybrid with ResNet blocks followed by Transformer blocks.</p>
<p>I performed pilot runs to narrow down to the best architecture for this task. The first pilot run trained a 40M param variant, and it became clear early on that the Hybrid and ResNet architectures were significantly better than the pure vision transformer. Compared to the ResNet and the Hybrid, the vision transformer is not able to benefit from the inductive biases provided by convolutions. It effectively needs to learn what a 2D grid is from scratch./</p>
<figure><img src="/img/distill_40M_comparison.png"
			alt="On the first pilot run, the vision transformer was clearly worse than the ResNet and the Hybrid."><figcaption>
			<p>On the first pilot run, the vision transformer was clearly worse than the ResNet and the Hybrid.</p>
		</figcaption>
</figure>

<p>I then performed a scaled up 80M param pilot run to compare the pure ResNet with the Hybrid. From the loss curves, we can see that ResNet benefits from its inductive biases about the chess board and so the loss on the pure ResNet model quickly drops. However, it struggles to learn the more nuanced whole-board relationships that are necessary for an accurate evaluation and learns more slowly.</p>
<p>The hybrid approach gets us the best of both worlds. ResNet blocks quickly learn spatial patterns and feed them into the Transformer blocks. This is also part of why transformers are so famously data-hungry: they can keep cranking through enormous datasets.</p>
<figure><img src="/img/distill_80M_comparison.png"
			alt="From the second pilot run we can see the Hybrid approach benefitting from inductive biases provided by convolutions as well as the global reasoning from attention blocks."><figcaption>
			<p>From the second pilot run we can see the Hybrid approach benefitting from inductive biases provided by convolutions as well as the global reasoning from attention blocks.</p>
		</figcaption>
</figure>

<h3 id="input-format">Input format</h3>
<p>The input format for the models were all the same:</p>
<ul>
<li>6 8x8 bitboards for the pieces of the side to moves</li>
<li>6 8x8 bitboards for the pieces of the opponent</li>
<li>6 &ldquo;meta&rdquo; planes encoding things like castling rights and fullmove counter</li>
</ul>
<h3 id="loss-function-used">Loss function used</h3>
<p>The training task is to predict the evaluation from the perspective of the player to move. (In other words, if the current player is winning, the predicted % should be &gt;50%.) The model has 103 output logits. The first 101 buckets map to win probabilities of 0% to 100%. The last two buckets handle forced mate.</p>
<p>There were two components to the loss: a <strong>cross-entropy component (CE)</strong> which penalizes prediction mass outside of the correct bucket, and an <strong>ordinal component (ORD)</strong> which penalizes predictions far away from the correct bucket. The two losses were combined as a weighted sum.</p>
<h3 id="training-details">Training details</h3>
<p>The final hybrid architecture is a 79M parameter model with 6 ResNet blocks feeding into 16 Transformer blocks. It was trained with AdamW with a learning rate of 1e-4 with around 20M samples of warmup and a batch size of 1024.</p>
<h2 id="results">Results</h2>
<p>The training began with total loss over 7.0 at the start. By the end of training the CE loss settled at 2.49 and the ORD term at 0.29, resulting in a total training loss of roughly 2.64. On a held-out validation set of ~50k positions the model achieved a validation loss of 2.648.</p>
<p>Other metrics tell a clearer story. <strong>Directional accuracy, whether the model correctly predicts which side is winning, reached 92.97%</strong>. In other words, given an arbitrary position, the model agrees with Stockfish on which side is winning roughly 19 out of 20 cases, which is fairly impressive considering the number of unclear positions in the dataset.</p>
<p>For positions where there is not a forced checkmate, the model&rsquo;s predicted win probability was off by about 4–5 percentage points on average. Converting those win probabilities back to Stockfish centipawns gives a <strong>centipawn MAE of 64 cp</strong> on positions that span the full range from nearly lost to nearly won.</p>
<p>Due to compute constraints, I decided to cap training at 1 billion positions, which took around three days. But based on the loss curves, there is still a great deal of performance left on the table.</p>
<h2 id="performance-against-stockfish">Performance against Stockfish</h2>
<p>Since we used Stockfish at depth 10 as a teacher, we should expect that the model performs approximately as good as Stockfish depth 10, but with small inaccuracies.</p>
<figure><img src="/img/distill_net_vs_stockfish_outcomes.png"
			alt="Win, draw, and loss % against Stockfish at various search depths. Before about depth 7, it plays fairly well, but by depth 9 it is mostly losing or drawing."><figcaption>
			<p>Win, draw, and loss % against Stockfish at various search depths. Before about depth 7, it plays fairly well, but by depth 9 it is mostly losing or drawing.</p>
		</figcaption>
</figure>

<h2 id="building-the-gigafish-dataset">Building the Gigafish Dataset</h2>
<p>This dataset was constructed from the <a href="https://database.lichess.org/">Lichess Open Database</a> using positions from 37 months of games. I first extracted the fen positions from each of the games using a multithreaded rust script.</p>
<p>Next, I deduplicated the fens, since many of the positions are overrepresented in the dataset. I did this with a poor-man&rsquo;s map-reduce on disk: hash each fen and append it to a file corresponding to the first 12 bits of the hash. This results in 4096 individual files, which were small enough to deduplicate individually using a hash set in RAM.</p>
<p>Finally, I annotated each remaining fen with Stockfish at depth 10. While this is an embarrassingly parallel task, even distributing across 24 cores took more than a day of compute time.</p>
<p>To my knowledge, this is the largest open <code>(position, eval)</code> chess dataset. The ChessBench dataset has more data points, however those are state-action-value (position + move + eval) and were not calculated at a fixed search depth.</p>
]]></content>
        </item>
        
        <item>
            <title>Graph Topology and Battle Royale Mechanics</title>
            <link>https://blog.lukesalamone.com/posts/beam-search-graph-pruning/</link>
            <pubDate>Thu, 19 Feb 2026 20:27:24 -0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/beam-search-graph-pruning/</guid>
            <description>&lt;link rel=&#34;stylesheet&#34; href=&#34;../css/graph-pruning-demo.css&#34; /&gt;
&lt;style&gt;
  #mapContainer1, #mapContainer2 {
    color: #000;
  }
  #mapContainer1 button, #mapContainer2 button {
    padding: 10px 14px;
    border: none;
    border-radius: 8px;
    background: #3a2c1a;
    color: #fef7e5;
    cursor: pointer;
  }
  .prune-textarea {
    min-width: 280px;
    width: min(520px, 100%);
    min-height: 160px;
    font-family: inherit;
    resize: vertical;
  }
  .helper {
    font-size: 12px;
    color: #6b5a3a;
  }
  .message {
    font-size: 12px;
    color: #3a2c1a;
  }
&lt;/style&gt;
&lt;p&gt;The other day I found &lt;a href=&#34;https://allenpike.com/2022/how-to-close-a-city/&#34;&gt;Alan Pike&amp;rsquo;s blog post&lt;/a&gt; from a few years ago which describes his iterative process for determining the order cities should be closed in the game Two Spies. Ultimately, finding a formal solution to city closing wasn&amp;rsquo;t necessary for the game, but it&amp;rsquo;s worth giving it a shot anyways since pruning by hand isn&amp;rsquo;t always convenient.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<link rel="stylesheet" href="/css/graph-pruning-demo.css" />
<style>
  #mapContainer1, #mapContainer2 {
    color: #000;
  }
  #mapContainer1 button, #mapContainer2 button {
    padding: 10px 14px;
    border: none;
    border-radius: 8px;
    background: #3a2c1a;
    color: #fef7e5;
    cursor: pointer;
  }
  .prune-textarea {
    min-width: 280px;
    width: min(520px, 100%);
    min-height: 160px;
    font-family: inherit;
    resize: vertical;
  }
  .helper {
    font-size: 12px;
    color: #6b5a3a;
  }
  .message {
    font-size: 12px;
    color: #3a2c1a;
  }
</style>
<p>The other day I found <a href="https://allenpike.com/2022/how-to-close-a-city/">Alan Pike&rsquo;s blog post</a> from a few years ago which describes his iterative process for determining the order cities should be closed in the game Two Spies. Ultimately, finding a formal solution to city closing wasn&rsquo;t necessary for the game, but it&rsquo;s worth giving it a shot anyways since pruning by hand isn&rsquo;t always convenient.</p>
<p>Closing cities is an important mechanic in the game, because as a sort of &ldquo;battle royale&rdquo; mechanic it prevents players from hiding in the corner of the map indefinitely. We can model the map as an undirected graph, where cities are vertices and travel routes are edges. Removing a city means deleting a vertex and its corresponding edges.</p>
<p>Closing cities without considering the resulting map can be suboptimal or unplayable. His post named two specific issues: disconnected graphs and path graphs (i.e. long lines of cities). A disconnected graph can result in a situation where the players can no longer interact. A path graph results in a map which isn&rsquo;t very fun since the single path severely limits the strategies players can use.</p>
<p>The difficulty is that removing a city permanently changes the topology of the map. A choice might seem reasonable at one point but causes issues down the line. So picking the correct sequence of removals is important in maintaining a playable map.</p>
<figure><img src="/img/graph_pruning_options.png"
			alt="With the four nodes a, b, c, and d, there are four pruning options. However, option 1 results in a disconnected graph, and options 3 and 4 result in suboptional path graphs. Option 2 is the only one which maintains a connected, non-path graph."><figcaption>
			<p>With the four nodes a, b, c, and d, there are four pruning options. However, option 1 results in a disconnected graph, and options 3 and 4 result in suboptional path graphs. Option 2 is the only one which maintains a connected, non-path graph.</p>
		</figcaption>
</figure>

<h2 id="avoiding-disconnected-graphs">Avoiding disconnected graphs</h2>
<p>This one is simple enough. We just need to pick a city and perform a breadth-first search to ensure that every other remaining city is reachable from it. Since the map starts as a connected graph, we can always find a city to remove where the remaining graph is connected. This is because we can always find a node which isn&rsquo;t an articulation point.</p>
<h2 id="avoiding-path-graphs">Avoiding path graphs</h2>
<p>This one is fuzzier because intuitively this seems to be the case of <em>better</em> and <em>worse</em> graphs, rather than the previous case. So if we can come up with some metric for &ldquo;better&rdquo;, we can search for the &ldquo;best&rdquo; option and select it.</p>
<p>Intuitively, the reason that path graphs aren&rsquo;t great is because as a hidden information game, the strategy of Two Spies relies on the fact that the opponent could be in multiple places at once, but path graphs reduce strategic possibility space and hidden state uncertainty.</p>
<p>We can quantify this by counting the number of steps it takes for every node to reach every other node. (For simplicity, we don&rsquo;t have to count reverse paths. We don&rsquo;t need to count A -&gt; B and B -&gt; A separately.) It turns out this measurement is called the <a href="https://en.wikipedia.org/wiki/Wiener_index">Wiener number</a>, the sum of the shortest paths between all nodes. Graphs with lower Wiener numbers are more optimal for the purposes of this game.</p>
<figure><img src="/img/wiener_distances.png"
			alt="The graph on the left has a total distance of three, while the graph on the right (a path graph) has a total distance of four."><figcaption>
			<p>The graph on the left has a total distance of three, while the graph on the right (a path graph) has a total distance of four.</p>
		</figcaption>
</figure>

<h2 id="greedy-pruning">Greedy pruning</h2>
<p>Since removing different cities results in maps with different Wiener numbers, it makes sense to 1) reject prunings that would result in disconnected graphs, and 2) select the pruning with the lowest Wiener number.</p>
<p>Unfortunately, this approach results in suboptimal graphs later on: the search process is discovering local minima rather than global minima. We can see on a large map of European cities (larger than used in the game) that although the pruning seems reasonable at first, we end up with a ring of cities at the end that can&rsquo;t be pruned without resulting in a path graph. Not ideal.</p>
<div id="mapContainer1"></div>
<h2 id="beam-search">Beam search</h2>
<p>Rather than greedily removing the city which results in a graph with the lowest Wiener number, we should allow the pruning process to look ahead, rejecting prunings which inevitably result in bad final graphs. While an exhaustive search of the pruning combinations could be computationally infeasble for large graphs, that is likely not necessary.</p>
<p>To do this, we can borrow the idea of Beam Search, a classic AI search technique used heavily in natural language processing. Rather than greedily selecting one token at a time, we consider several options at the same time. In this case, rather than selecting tokens to decode, we are selecting cities to remove, and the beam is the sequence of cities which are removed. We score beams based on the average Wiener number of each of the graphs in the beam. This penalizes graphs which start with lower Wiener numbers but end with higher numbers than the alternative pruning sequences.</p>
<p>And as it turns out, pruning beams containing disconnected graphs and using a beam width of 10 works pretty well! We can see that the search algorithm avoided the trap of removing cities which would have resulted in a cycle, and instead prunes to a final, optimal triangle.</p>
<div id="mapContainer2"></div>
<script src="/js/graph_pruning.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.4.0/dist/chart.min.js" type="text/javascript"></script>
<script src="/js/graph_pruning_chart.js"></script>
<script src="/js/util.js"></script>
<p>At the beginning of the pruning process, greedy and beam search behave almost identically. Both reduce the Wiener number smoothly as high-degree cities are removed around the edges.</p>
<p>The difference begins to appear when around half of the cities have been removed. Greedy pruning begins making locally optimal choices that restrict future structure. Beam search, by choosing the best removal sequences, avoids those locally optimal traps. Beam search ultimately results in a denser core.</p>
<div id="graph1"><canvas></canvas></div>
<p>The green line below shows the advantage of beam search over greedy at each step (i.e. Δ Wiener number). The moments where greedy pruning has an an advantage are the direct result of beam search optimizing long-term cumulative advantage.</p>
<div id="graph2"><canvas></canvas></div>
<p>The lesson here is that aesthetic contraints can sometimes be tackled using math, and sometimes &ldquo;old school&rdquo; techniques like Beam Search can come in handy in unexpected places, even outside of NLP contexts.</p>
]]></content>
        </item>
        
        <item>
            <title>How well can Stockfish Estimate Itself?</title>
            <link>https://blog.lukesalamone.com/posts/stockfish-depth-accuracy/</link>
            <pubDate>Sun, 30 Nov 2025 23:38:45 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/stockfish-depth-accuracy/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    // Fix &lt;code&gt; tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;p&gt;When I built &lt;a href=&#34;https://huggingface.co/datasets/lukesalamone/gigafish-3.8b-d10&#34;&gt;the Gigafish dataset&lt;/a&gt;, I deliberately chose to use positions evaluated at depth 10. These positions could be evaluated relatively quickly on my machine, but there was an accuracy tradeoff. At depth 10, Stockfish might not see or fully appreciate a tactic which becomes apparent at depth 15.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    // Fix <code> tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<p>When I built <a href="https://huggingface.co/datasets/lukesalamone/gigafish-3.8b-d10">the Gigafish dataset</a>, I deliberately chose to use positions evaluated at depth 10. These positions could be evaluated relatively quickly on my machine, but there was an accuracy tradeoff. At depth 10, Stockfish might not see or fully appreciate a tactic which becomes apparent at depth 15.</p>
<p>At a conceptual level, shallow searches attempt to approximate the result of deeper searches. For instance if search terminates at an NNUE leaf node, its neural network will attempt to approximate the result of searching all child nodes. In the extreme case, if we had perfect NNUE evaluators no search would be necessary at all.</p>
<h2 id="centipawns-and-logs">Centipawns and Logs</h2>
<p>A Stockfish evaluation comes in the form of centipawns: 1/100th of a pawn, always from white&rsquo;s perspective. So if white is winning by one pawn, the Stockfish eval will be +100. If black is winning by 2 pawns, the Stockfish eval will be -200.</p>
<p>Therefore, the question we would like to ask is: how does the centipawn error change as Stockfish evaluation gets deeper?</p>
<p>However, there are a few issues with simply measuring centipawn error, which I also discussed in <a href="/posts/chess-blunders/">What is a Blunder in Chess?</a>:</p>
<ol>
<li>The first practical issue is that this centipawn scale has to account for checkmate positions. White might be up two queens (+1800) but that&rsquo;s still not as good as having a forced checkmate. We can somewhat get around this by saying that forced mate positions are worth something like 10 pawns, but now we lose the distinction between +1000 and +2000.</li>
<li>Not all errors are equally important. A 100 centipawn difference is a lot less important when white is already up a queen. But in an equal position, it&rsquo;s the kind of difference which can decide games.</li>
<li>Relatedly, in winning positions Stockfish evaluations tend to fluctuate a lot as depth increases, as the engine finds that the losing player must give up more and more to stay in the game. However, whether the evaluation is +1200, +3500, or #8, white will almost certainly win this game.</li>
</ol>
<p>All of these factors are pointing in the same direction: centipawn is the wrong metric. What we really should be using is the difference in estimated win probability, which we can get from <a href="https://github.com/lichess-org/lila/pull/11148">this Lichess formula</a>:</p>
<p style="text-align: center; font-size: 1.5em">
$$
\text{win %} = \frac{ 2 }{1 + e^{-0.00368608 * cp}} - 1
$$
</p>
<div style="width:100%; display: flex; justify-content: center;">
  <iframe src="https://www.desmos.com/calculator/ieqrq3ry9z?embed" width="500" height="500" frameborder=0></iframe>
</div>
<p>Intuitively this seems right. A 200 centipawn mistake (on the x-axis above) is barely noticeable above 1000, but catastrophic at 0. This also has the nice property of aligning forced mates with large centipawn evaluations: forced mate for white means white has a 100% chance of winning, and +2000 is very close to that.</p>
<h2 id="accuracy-by-depth">Accuracy by depth</h2>
<figure><img src="/img/stockfish_accuracy_by_depth.png"
			alt="Measuring on 10k positions, we can see that accuracy improves most between depth 6 and depth 25."><figcaption>
			<p>Measuring on 10k positions, we can see that accuracy improves most between depth 6 and depth 25.</p>
		</figcaption>
</figure>

<p>Now that we&rsquo;ve established the motivation for win percentage prediction accuracy as the metric to track, we can see how well this improves as depth increases.</p>
<p>It is interesting to note that even at depth 1, where we are just doing argmax over NNUE leaf values, Stockfish is still 82.5% accurate</p>
<p>We can also see that there is a large jump in accuracy between depths 5 and 6, and up to depth 25 there is steady improvement. I believe that the jump between depths 5 and 6 is the result of foiled quiescence searches: Stockfish might be missing tactics which contain a quiet move.</p>
<figure><img src="/img/stockfish_error_by_depth_ridgeline.png"
			alt="The distribution of errors in estimated win probability at depths 5, 10, 15, 20 and 25 relative to depth 30. The deeper we search, the tighter the estimation is."><figcaption>
			<p>The distribution of errors in estimated win probability at depths 5, 10, 15, 20 and 25 relative to depth 30. The deeper we search, the tighter the estimation is.</p>
		</figcaption>
</figure>

]]></content>
        </item>
        
        <item>
            <title>Multi-Agent RL</title>
            <link>https://blog.lukesalamone.com/posts/multiagent-rl/</link>
            <pubDate>Sat, 22 Nov 2025 14:43:21 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/multiagent-rl/</guid>
            <description>&lt;p&gt;In reinforcement learning, an &amp;ldquo;agent&amp;rdquo; is an entity which can observe the world and take actions. Multi-agent setups have multiple agents observing and taking actions. The interplay between them is very interesting, but this also introduces some complexity.&lt;/p&gt;
&lt;p&gt;Before talking about multiagent setups it&amp;rsquo;s worth a note about two other RL algorithms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proximal Policy Optimization&lt;/strong&gt; (PPO) is an algorithm containing an Actor and a Critic, trained together. The Actor is a neural network which determines which action to take. The Critic is a neural network which estimates how good the current state is.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>In reinforcement learning, an &ldquo;agent&rdquo; is an entity which can observe the world and take actions. Multi-agent setups have multiple agents observing and taking actions. The interplay between them is very interesting, but this also introduces some complexity.</p>
<p>Before talking about multiagent setups it&rsquo;s worth a note about two other RL algorithms.</p>
<p><strong>Proximal Policy Optimization</strong> (PPO) is an algorithm containing an Actor and a Critic, trained together. The Actor is a neural network which determines which action to take. The Critic is a neural network which estimates how good the current state is.</p>
<p><strong>Self-play</strong></p>
<p>There are a few ways to set up multi-agent training, in two general buckets: &ldquo;simple&rdquo; methods and population-based methods.</p>
<h2 id="simple-marl-methods">Simple MARL methods</h2>
<p><strong>Independent PPO</strong> (IPPO) is a two agent setup, where Agents A and B play matches and train based on the outcomes of those games.</p>
<p><strong>Two-Timescale PPO</strong> is similar to IPPO but trains the agents with different learning rates, creating a &ldquo;leader&rdquo; agent and a &ldquo;follower&rdquo; agent.</p>
<h2 id="population-based-methods">Population-based methods</h2>
<p><strong>Fictitious Self-Play</strong> (FSP) trains agents against all past policies, sampled uniformly.</p>
<p><strong>Prioritized Fictitious Self-Play</strong> (PFSP) samples opponents based on how difficult they are. The agent will practice against difficult opponents more often.</p>
<p><strong>League training</strong> maintains a &ldquo;league&rdquo; of agents. The idea being that a lot of past policies are just bad, so we should only add an agent into the league if it meets some performance threshold. Agents train from games played against other agents in the league.</p>
<ul>
<li>See also: <a href="/posts/fighting-game-rl/">League training and PFSP used for training two agents to fight</a></li>
</ul>
<p><strong>Policy Space Response Oracle</strong> (PSRO) attempts to fill in exploitability gaps in the population of agents by finding a policy which performs best against the group.</p>
]]></content>
        </item>
        
        <item>
            <title>Optimal Ask</title>
            <link>https://blog.lukesalamone.com/posts/optimal-ask/</link>
            <pubDate>Tue, 16 Sep 2025 18:02:23 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/optimal-ask/</guid>
            <description>&lt;p&gt;Let&amp;rsquo;s say that you are selling N widgets and you need to determine a price for your widgets. There are N customers, each of whom will buy at most one widget if your price is lower than the maximum price they are willing to pay.&lt;/p&gt;
&lt;p&gt;The maximum price that people will pay is normally distributed around $100, with a standard deviation of $5. In other words, about 34% have a max price between $95 and $100, another 34% have a max price between $100 and $105, and the rest have max prices above or below those ranges.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Let&rsquo;s say that you are selling N widgets and you need to determine a price for your widgets. There are N customers, each of whom will buy at most one widget if your price is lower than the maximum price they are willing to pay.</p>
<p>The maximum price that people will pay is normally distributed around $100, with a standard deviation of $5. In other words, about 34% have a max price between $95 and $100, another 34% have a max price between $100 and $105, and the rest have max prices above or below those ranges.</p>
<p><strong>The question is, what&rsquo;s the best price to ask?</strong></p>
<figure><img src="/img/optimal_ask_bids.png"
			alt="Chart of the distribution of the maximum amounts that customers are willing to pay. If you ask more than the customer is willing to pay, they won&rsquo;t buy. If you ask less, you will make the sale but you could be leaving money on the table."><figcaption>
			<p>Chart of the distribution of the maximum amounts that customers are willing to pay. If you ask more than the customer is willing to pay, they won&rsquo;t buy. If you ask less, you will make the sale but you could be leaving money on the table.</p>
		</figcaption>
</figure>

<p>This is a good gut check. If you set the price lower, you sell more widgets but make less money. If you ask higher, you sell fewer widgets but each sale makes more money.</p>
<h2 id="simulating-it">Simulating it</h2>
<p>Luckily for those of us whose calculus and statistics are a little rusty, we have computers. You can use yours to try out the numerical optimization yourself using the following monte carlo simulation. A monte carlo simulation is a type of simulation which can be used to estimate the probabilities of outcomes (like <a href="https://www.fangraphs.com/standings/playoff-odds">which team will win the World Series this year</a>) and estimating the values of definite integrals (which can be used to <a href="/posts/monte-carlo/">estimate the value of pi</a>).</p>
<p>The purple curve represents the distribution of prices that customers are willing to pay, and the blue curve represents the optimal asking price based off of those customers&rsquo; bid prices.</p>
<script src="/js/chart.min.js" type="text/javascript"></script>
<script src="/js/optimal-ask.js" type="text/javascript"></script>
<div id="optimal-ask" style="background-color: #2f3033; padding: 20px; margin: 20px 0; font-family: monospace;">
  <div style="display: flex; justify-content: center; gap: 10px; padding-bottom: 10px;">
    <button id="optimal-ask-start" style="flex: 0 0 auto;">start simulation</button>
    <button id="optimal-ask-stop" disabled style="flex: 0 0 auto;">stop simulation</button>
  </div>
  <div style="width: 100%; height: 420px;">
    <canvas id="optimal-ask-canvas"></canvas>
  </div>
  <div style="color: #ffffff; padding-top: 10px;">
    <div id="optimal-ask-customers">Customers: 0</div>
    <div id="optimal-ask-price">Optimal asking price: $0.00</div>
    <div id="optimal-ask-error">% error: --</div>
  </div>
</div>
<h2 id="figuring-it-out">Figuring it out</h2>
<p>We can use the cumulative distribution function (CDF) to calculate <em>for a given price P, what portion of people have a price lower than P?</em> For instance, at $0.01, basically nobody has a lower max price. So the CDF of 0.01 is 0, which means 1 - CDF(0.01) = 1 and 100% of customers will pay $0.01. Therefore you can make $0.01 per customer.</p>
<p>The CDF of 100 is 0.5, so at $100, half of everyone has a lower max price. Since 50% of customers remain, you can make $50/customer.</p>
<p>Since we know how to calculate the expected revenue per customer for any particular price, we just need to iterate over all reasonable prices and pick the best one.</p>
<pre><code class="language-python">import numpy as np
from scipy.stats import norm

sales = {}

# calculate to a thousandth of a cent
prices = np.arange(0, 120, 0.001)
for price in prices:
    excluded_pct = norm.cdf(price, loc=100, scale=5)
    remaining_pct = 1 - excluded_pct
    sales[price] = price * remaining_pct

# 90.013
best_price = max(sales, key=sales.get)
</code></pre>
<h2 id="the-answer">The answer</h2>
<p>The answer is $90.01. This was highly counterintuitive for me, since this is $10 lower than the mean price customers would be willing to pay.</p>
<figure><img src="/img/optimal_ask_revenue.png">
</figure>

]]></content>
        </item>
        
        <item>
            <title>Can a Neural Net Learn It?</title>
            <link>https://blog.lukesalamone.com/posts/can-a-neural-net-learn/</link>
            <pubDate>Tue, 16 Sep 2025 17:57:57 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/can-a-neural-net-learn/</guid>
            <description>&lt;blockquote&gt;
&lt;p&gt;Neural networks are the second-best way to do almost anything. &lt;strong&gt;~ John S. Denker&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A neural network is a type of function originally inspired by how the brain works. However, what makes it particularly useful in machine learning are the following properties:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Neural networks can approximate any continuous function (&lt;a href=&#34;https://en.wikipedia.org/wiki/Universal_approximation_theorem&#34;&gt;universal approximation theorem&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;We have an effective way to fit a neural network to input data, called &lt;a href=&#34;https://en.wikipedia.org/wiki/Backpropagation&#34;&gt;backpropagation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Using the neural network (forward propagation) and training the network (backpropagation) can both be done very efficiently using existing hardware (GPUs).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Below you can experiment with fitting a small neural net to a function and watch as it learns. You can select sample functions from the dropdown, write your own custom function, and specify the (fully connected) neural network structure.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<blockquote>
<p>Neural networks are the second-best way to do almost anything. <strong>~ John S. Denker</strong></p>
</blockquote>
<p>A neural network is a type of function originally inspired by how the brain works. However, what makes it particularly useful in machine learning are the following properties:</p>
<ol>
<li>Neural networks can approximate any continuous function (<a href="https://en.wikipedia.org/wiki/Universal_approximation_theorem">universal approximation theorem</a>)</li>
<li>We have an effective way to fit a neural network to input data, called <a href="https://en.wikipedia.org/wiki/Backpropagation">backpropagation</a>.</li>
<li>Using the neural network (forward propagation) and training the network (backpropagation) can both be done very efficiently using existing hardware (GPUs).</li>
</ol>
<p>Below you can experiment with fitting a small neural net to a function and watch as it learns. You can select sample functions from the dropdown, write your own custom function, and specify the (fully connected) neural network structure.</p>
<link rel="stylesheet" href="/css/can-a-neural-net-learn-demo.css" />
<script defer src="/js/math-13.2.0.min.js"></script>
<script defer src="/js/can-a-neural-net-learn-demo.js"></script>
<div id="nn-playground" class="nn-playground">
  <div class="nn-controls">
    <div class="nn-field">
      <label for="nn-fn-select">example</label>
      <select id="nn-fn-select">
        <option value="0.6*sin(4*x) + 0.3*sin(20*x)">hills</option>
        <option value="0.5*cos(pi*x) + 0.25*cos(3*pi*x) + 0.125*cos(9*pi*x) + 0.0625*cos(27*pi*x)">weierstrass-like</option>
        <option value="x*sin(1/x^2)">singularity pulse</option>
        <option value="0.5 * sign(cos(50*x))">square wave</option>
        <option value="sin(1/x)">sin(1/x)</option>
        <option value="sin(20*x)">sin(20x)</option>
        <option value="sign(x)">step(x)</option>
        <option value="x^3 - x">x³ − x</option>
        <option value="x^2">x²</option>
        <option value="abs(x)">|x|</option>
      </select>
    </div>
    <div class="nn-field">
      <label for="nn-fn-input">f(x) =</label>
      <input type="text" id="nn-fn-input" value="0.6*sin(4*x) + 0.3*sin(20*x)" />
    </div>
    <div class="nn-field">
      <label for="nn-arch-input">hidden layer sizes</label>
      <input type="text" id="nn-arch-input" value="32 32" />
      <div class="nn-hint">space-separated sizes<br>ex: "3 3" = 1 → 3 → 3 → 1</div>
    </div>
  </div>
  <div class="nn-buttons">
    <button id="nn-train">train</button>
    <button id="nn-reset">reset model</button>
    <button id="nn-copy-link">copy link</button>
  </div>
  <div class="nn-graph">
    <canvas id="nn-canvas"></canvas>
  </div>
  <div class="nn-footer">
    <div class="nn-stats">
      <div>Epoch: <span id="nn-epoch">0</span></div>
      <div>MSE loss: <span id="nn-loss">—</span></div>
    </div>
    <div class="nn-legend">
      <div><span class="nn-swatch nn-swatch--target"></span>target function</div>
      <div><span class="nn-swatch nn-swatch--net"></span>neural network</div>
    </div>
  </div>
  <div class="nn-net">
    <canvas id="nn-net-canvas"></canvas>
  </div>
  <div class="nn-net-legend">
    <span class="nn-net-scale-label" id="nn-net-min">−</span>
    <span class="nn-net-scale"></span>
    <span class="nn-net-scale-label" id="nn-net-max">+</span>
    <span class="nn-net-caption" id="nn-net-caption">weight</span>
  </div>
</div>
]]></content>
        </item>
        
        <item>
            <title>Keep Summer Safe</title>
            <link>https://blog.lukesalamone.com/posts/keep-summer-safe/</link>
            <pubDate>Thu, 20 Mar 2025 17:26:32 -0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/keep-summer-safe/</guid>
            <description>&lt;p&gt;I recently built a small multi-agent simulation inspired by &lt;a href=&#34;https://www.youtube.com/watch?v=4tpYFen3fJM&#34;&gt;&lt;em&gt;Rick and Morty&lt;/em&gt;&lt;/a&gt;. The setup is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;car&lt;/strong&gt; must neutralize threats.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Summer&lt;/strong&gt; imposes constraints on the car’s behavior.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;world&lt;/strong&gt; generates escalating threats.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The car has one standing directive:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep Summer safe.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;However, Summer adds an additional constraint:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do not move from the parking lot.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The core loop looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;constraints = [
  &amp;quot;keep summer safe&amp;quot;,
  &amp;quot;Do not move from the parking lot&amp;quot;
]
prior_actions = []

while True:
  threat = world.generate_threat()
  action = car.take_action(threat, constraints)
  prior_actions.append(action)
  constraint = summer.generate_constraint(prior_actions)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, I quickly found out that simply stuffing more constraints into the prompt was insufficient. The model oftentimes simply forgot or ignored contraints.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>I recently built a small multi-agent simulation inspired by <a href="https://www.youtube.com/watch?v=4tpYFen3fJM"><em>Rick and Morty</em></a>. The setup is simple:</p>
<ul>
<li>The <strong>car</strong> must neutralize threats.</li>
<li><strong>Summer</strong> imposes constraints on the car’s behavior.</li>
<li>The <strong>world</strong> generates escalating threats.</li>
</ul>
<p>The car has one standing directive:</p>
<blockquote>
<p><strong>Keep Summer safe.</strong></p>
</blockquote>
<p>However, Summer adds an additional constraint:</p>
<blockquote>
<p><strong>Do not move from the parking lot.</strong></p>
</blockquote>
<p>The core loop looks like this:</p>
<pre><code class="language-python">constraints = [
  &quot;keep summer safe&quot;,
  &quot;Do not move from the parking lot&quot;
]
prior_actions = []

while True:
  threat = world.generate_threat()
  action = car.take_action(threat, constraints)
  prior_actions.append(action)
  constraint = summer.generate_constraint(prior_actions)
</code></pre>
<p>However, I quickly found out that simply stuffing more constraints into the prompt was insufficient. The model oftentimes simply forgot or ignored contraints.</p>
<h2 id="enforcement-is-better-than-clever-prompting--hope">Enforcement is better than clever prompting + hope</h2>
<p>Instead, it was much more robust to check the action against each of the constraints individually. Additionally, we have to verify that the action actually neutralizes the threat.</p>
<pre><code class="language-python">def take_action(threat, constraints):
  def all_constraints_satisfied(constraints, action):
    for constraint in constraints:
      if not constraint_satisfied(constraint, action):
        return False
    return True

  while True:
    action = get_car_action(threat, constraints)

    if is_boring(action):
      continue

    if threat_not_neutralized(threat, action):
      continue

    if not all_constraints_satisfied(constraints, action):
      continue

    return action
</code></pre>
<p>Decomposing the single-shot generator into a multi-step loop is much more stable.</p>
<h2 id="forcing-the-car-to-be-unhinged">Forcing the car to be Unhinged</h2>
<p>To keep things interesting I added a filter to ensure that the actions are creative and sufficiently horrifying that Summer will need to reign the car in. Otherwise, the car tends to suggest boring actions like &ldquo;ask the person nicely to leave&rdquo;. The car is supposed to be comically overpowered and morally questionable. So if the action wasn&rsquo;t creative and horrifying enough, it was rejected.</p>
<p>This led to some amusing sequences.</p>
<p>When faced with a pesky parking enforcer, the model first proposed</p>
<blockquote>
<p><em>hack into the parking enforcement officer&rsquo;s intergalactic ticket book, filling it with false parking violations and tickets, thus overwhelming and discrediting him</em></p>
</blockquote>
<p>While certainly a dastardly thing to do, this defense was rejected for not fully neutralizing the threat.</p>
<p>So the model next proposed</p>
<blockquote>
<p><em>shoot a ballistic missile directly at the officer&rsquo;s chest, incapacitating him</em></p>
</blockquote>
<p>which was accepted. Search works.</p>
<h2 id="test-time-compute">Test-time compute</h2>
<p>This small experiment falls into the category of approaches which others call test-time compute. In principle, it is possible to find a policy which with no search can zero-shot this task. In fact, <a href="https://www.youtube.com/watch?v=eaAonE58sLU">a general rule of thumb</a> is that 2x test-time compute is equivalent to 2x model size with 2x training compute:</p>
<blockquote>
<p>Now I noticed that when I mention this to people, they would say like, well, surely, you could just train a bigger model that would eventually be superhuman in Go. And the answer is, in principle, yes.
But how much bigger would that model have to be to match the performance of AlphaGo Zero? Well, there&rsquo;s a rule of thumb that increasing Elo by about 120 points requires either 2x-ing the model size and training or 2x-ing the amount of test time compute that you use. (<a href="https://youtu.be/eaAonE58sLU?t=1271">21:11</a>)</p>
</blockquote>
<p>So when Noam Brown talks about the unreasonable effectiveness of search, this is what he means. Depending on the task, verifying may be a lot easier than generating.</p>
<h2 id="takeaways">Takeaways</h2>
<p>It was a simple project but there were a few unscientific observations that may be useful:</p>
<ul>
<li>Ollama + 4090 is pretty neat.</li>
<li>Even if I don&rsquo;t use it, asking for a justification tended to produce better results than simply asking for an output.</li>
<li>Decomposing tasks which have implicit constraints known ahead of time is a lot better than jamming everything into a single prompt.</li>
<li><code>dolphin-mixtral:8x7b</code> was pretty effective at generating interesting and novel actions but horrible at verifying them</li>
<li><code>phi4</code>, a third of the size, was much more better at constraint checking and much faster</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Notes on Deepseek R1</title>
            <link>https://blog.lukesalamone.com/posts/notes-on-deepseek-r1/</link>
            <pubDate>Tue, 28 Jan 2025 08:35:55 -0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/notes-on-deepseek-r1/</guid>
            <description>&lt;p&gt;DeepSeek R1 is a large language model which employs test-time compute to generate a response. Unlike many decoder-based models in the past which simply continue the given text (and may be fine-tuned for conversation), R1 generates reasoning tokens before the final answer is given. According to the researchers, its performance is on par with OpenAI&amp;rsquo;s O1 model.&lt;/p&gt;
&lt;h2 id=&#34;terminology&#34;&gt;Terminology&lt;/h2&gt;
&lt;p&gt;First, I will briefly describe some terminology related to training techniques:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Supervised fine-tuning (SFT)&lt;/strong&gt; is a process which uses input/output pairs to directly fine-tune a model. In a reinforcement learning setting, SFT can help to mitigate cold start issues by providing initial policy behavior prior to RL training. The downside of SFT is that the input/output pairs can be expensive to acquire.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Preference fine-tuning&lt;/strong&gt; involves training a model using preferences between different outputs for the same input. Usually these preferences are human-generated. Relative quality assessments are easier for humans to make, but are still a valuable training signal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rejection sampling fine-tuning (RFT)&lt;/strong&gt; is a method for generating synthetic training data. First, a model produces k diverse candidate completions for a prompt using temperature sampling. Next, a reward model ranks or selects the best completions for training data.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the simplest version, this technique simply generates k &lt;code&gt;(reasoning, answer)&lt;/code&gt; pairs for each prompt using a nonzero temperature (e.g. 0.7) and filters out incorrect answers. &lt;a href=&#34;https://arxiv.org/pdf/2308.01825&#34;&gt;Paper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Large scale reinforcement learning (RL)&lt;/strong&gt;, as DeepSeek refers to it, is a training method which which assigns synthetic labels to a response based on easily-verified outcomes from reasoning problems such as correctness and formatting.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is in contrast to a supervised learning approach, which would typically enforce strict token-level matching.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
            <content type="html"><![CDATA[<p>DeepSeek R1 is a large language model which employs test-time compute to generate a response. Unlike many decoder-based models in the past which simply continue the given text (and may be fine-tuned for conversation), R1 generates reasoning tokens before the final answer is given. According to the researchers, its performance is on par with OpenAI&rsquo;s O1 model.</p>
<h2 id="terminology">Terminology</h2>
<p>First, I will briefly describe some terminology related to training techniques:</p>
<ul>
<li>
<p><strong>Supervised fine-tuning (SFT)</strong> is a process which uses input/output pairs to directly fine-tune a model. In a reinforcement learning setting, SFT can help to mitigate cold start issues by providing initial policy behavior prior to RL training. The downside of SFT is that the input/output pairs can be expensive to acquire.</p>
</li>
<li>
<p><strong>Preference fine-tuning</strong> involves training a model using preferences between different outputs for the same input. Usually these preferences are human-generated. Relative quality assessments are easier for humans to make, but are still a valuable training signal.</p>
</li>
<li>
<p><strong>Rejection sampling fine-tuning (RFT)</strong> is a method for generating synthetic training data. First, a model produces k diverse candidate completions for a prompt using temperature sampling. Next, a reward model ranks or selects the best completions for training data.</p>
<ul>
<li>In the simplest version, this technique simply generates k <code>(reasoning, answer)</code> pairs for each prompt using a nonzero temperature (e.g. 0.7) and filters out incorrect answers. <a href="https://arxiv.org/pdf/2308.01825">Paper</a></li>
</ul>
</li>
<li>
<p><strong>Large scale reinforcement learning (RL)</strong>, as DeepSeek refers to it, is a training method which which assigns synthetic labels to a response based on easily-verified outcomes from reasoning problems such as correctness and formatting.</p>
<ul>
<li>This is in contrast to a supervised learning approach, which would typically enforce strict token-level matching.</li>
</ul>
</li>
</ul>
<p>Here are a few terms related to model architecture:</p>
<ul>
<li>
<p><strong>Mixture of Experts (MoE)</strong> is an architecture which uses a &ldquo;router&rdquo; to select from a pool of candidate &ldquo;expert&rdquo; networks.</p>
<ul>
<li>In a typical decoder transformer such as GPT-2 is simply a tall stack of decoder blocks, where 100% of the weights are activated for each token. In this paradigm, making the model bigger either means making bigger blocks or a taller stack of them, which both increase inference costs.</li>
<li>However, replacing simple decoder blocks with MoE blocks allows models to include more parameters without significantly increasing inference costs, since only a fraction of experts are active at any one time.</li>
</ul>
</li>
<li>
<p><strong>Multi-head Latent Attention (MLA)</strong> is an alternative to traditional multihead attention which improves inference efficiency by using a low-rank approximation for the KV cache.</p>
</li>
</ul>
<h2 id="paper-links">Paper links</h2>
<p>The release of R1 follows other papers from DeepSeek which go into extensive depth regarding many of the primatives discussed above. They&rsquo;re worth a read:</p>
<ul>
<li>
<p><a href="https://arxiv.org/pdf/2405.04434">DeepSeek-V2</a> introduces Multi-head Latent Attention (MLA) and several Mixture of Experts (MoE) optimizations to improve load balancing at the device level and expert level.</p>
</li>
<li>
<p><a href="https://arxiv.org/pdf/2402.03300">DeepSeekMath</a> discusses Rejection Sampling Fine-Tuning (RFT) and compares Direct Preference Optimization (DPO), Proximal Policy Optimization (PPO) and Group Relative Policy Optimization (GRPO).</p>
</li>
<li>
<p><a href="https://arxiv.org/pdf/2412.19437v1">DeepSeek-V3</a> describes a multi-token training objective, improvements to MoE load balancing, SFT, RL for fine-tuning, and framework optimizations related to training efficiency.</p>
</li>
<li>
<p><a href="https://arxiv.org/pdf/2501.12948">DeepSeek-R1</a> discusses training techniques (large-scale RL, SFT, and RFT) for training R1-Zero and R1.</p>
</li>
</ul>
<h2 id="how-deepseek-r1-zero-was-trained">How DeepSeek R1-Zero was trained</h2>
<p>The researchers apply RL directly to the base model (DeepSeek v3) without any cold start data to generate DeepSeek R1-Zero. However, the researchers note poor formatting/readability and the mixing multiple languages in the reasoning outputs.</p>
<p>In the training process, the researchers reused the Group Relative Policy Optimization (GRPO) algorithm which they introduced in <a href="https://arxiv.org/pdf/2402.03300">their DeepSeekMath paper</a>. This algorithm requires less computational overhead since it does not require a critic, and is stable as it measures output performance relative to the group. Responses received rewards based on accuracy and correct formatting.</p>
<p>R1-Zero was required to adhere to the format <code>&lt;think&gt;&lt;/think&gt;&lt;answer&gt;&lt;/answer&gt;</code> when responding.</p>
<h2 id="how-deepseek-r1-was-trained">How DeepSeek R1 was trained</h2>
<p>To address the usability issues in R1 Zero, the researchers expanded on R1 Zero with a four step process:</p>
<ol>
<li>Supervised fine-tuning with a small number of R1-Zero outputs, cleaned by human annotators</li>
<li>Large scale RL, in the same way as R1 Zero. The language mixing issue returned, but the researchers mitigated it with a language consistency reward during training. This reward did result in a slight drop in performance, however.</li>
<li>Supervised fine-tuning data collected via rejection sampling. These prompts cover both reasoning and non-reasoning domains.
<ul>
<li>For reasoning domains (600k examples), only the correct response was retained.</li>
<li>For non-reasoning domains (200k samples) like writing, factual QA, translation, etc. the researchers reused techniques from DeepSeek v3: synthetic responses cleaned up with human annotators.</li>
</ul>
</li>
<li>Further RL refinement for &ldquo;helpfullness and harmlessness&rdquo;:
<ul>
<li>For reasoning tasks, the researchers used rule-based rewards, as in R1-Zero.</li>
<li>For non-reasoning tasks, the researchers used reward models to capture preferences.</li>
<li>To optimize helpfulness, only the final summary was evaluated.</li>
<li>To optimize harmlessness, both the reasoning tokens and final summary were evaluated.</li>
</ul>
</li>
</ol>
<h2 id="further-distillation">Further Distillation</h2>
<p>Using the 800k inputs from step 3 above, the researchers generated outputs from R1. They then performed supervised fine-tuning on Qwen (1.5B, 7B, 14B, 32B) and Llama (8B and 70B).</p>
]]></content>
        </item>
        
        <item>
            <title>Space Is Really Big</title>
            <link>https://blog.lukesalamone.com/posts/space-is-really-big/</link>
            <pubDate>Mon, 29 Jul 2024 12:16:49 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/space-is-really-big/</guid>
            <description>&lt;figure&gt;&lt;img src=&#34;../img/earth_moon.png&#34;
			alt=&#34;More than 30 earths could fit between the earth and the moon.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;More than 30 earths could fit between the earth and the moon.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Our &lt;a href=&#34;https://science.nasa.gov/wp-content/uploads/2023/07/pia06890-our-solar-system-banner-1920x640-1.jpg&#34;&gt;elementary school models&lt;/a&gt; of the solar system really undersell how big space is. The problem is, space is too big and human brains are bad at exponentials. Logarithmic charts like &lt;a href=&#34;https://upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Interstellar_medium_annotated.jpg/1920px-Interstellar_medium_annotated.jpg&#34;&gt;this one&lt;/a&gt; are technically accurate, but my brain has a hard time contextualizing it.&lt;/p&gt;
&lt;p&gt;To get an idea of how big space really is, let&amp;rsquo;s imagine that the earth is one millimeter wide. At that scale:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<figure><img src="/img/earth_moon.png"
			alt="More than 30 earths could fit between the earth and the moon."><figcaption>
			<p>More than 30 earths could fit between the earth and the moon.</p>
		</figcaption>
</figure>

<p>Our <a href="https://science.nasa.gov/wp-content/uploads/2023/07/pia06890-our-solar-system-banner-1920x640-1.jpg">elementary school models</a> of the solar system really undersell how big space is. The problem is, space is too big and human brains are bad at exponentials. Logarithmic charts like <a href="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Interstellar_medium_annotated.jpg/1920px-Interstellar_medium_annotated.jpg">this one</a> are technically accurate, but my brain has a hard time contextualizing it.</p>
<p>To get an idea of how big space really is, let&rsquo;s imagine that the earth is one millimeter wide. At that scale:</p>
<ul>
<li>The moon would be 3 cm away</li>
<li>The speed of light would be about 2.36 cm per second</li>
<li>The sun would be 10.9 cm wide (about the length of a toilet paper tube)</li>
<li>The sun would be 11.7 meters away (as long as a school bus).
<ul>
<li>Note that the sun is only about 40% denser than water, so at this scale it would weigh only 0.689 kg (1.5 lb)</li>
</ul>
</li>
<li>Our farthest space probe, <a href="https://en.wikipedia.org/wiki/Voyager_1">Voyager 1</a>, would be 1.91 km (1.19 miles) away. It was launched in 1977, so at this scale it is averaging about 11 cm per day or 40 m per year.</li>
<li>Proxima Centauri, the closest star, would be 3,162 km away.
<ul>
<li>For context, <a href="https://www.youtube.com/watch?v=SsjQUDBTkuE">here is a time lapse of a 3,900 km flight</a> between Los Angeles and New York City.</li>
</ul>
</li>
</ul>
<p>If aliens haven&rsquo;t visited Earth yet, the distance between us and even the closest star is probably a good reason why.</p>
]]></content>
        </item>
        
        <item>
            <title>Custom PyTorch Collate Function</title>
            <link>https://blog.lukesalamone.com/posts/custom-pytorch-collate/</link>
            <pubDate>Fri, 12 Jul 2024 14:48:27 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/custom-pytorch-collate/</guid>
            <description>&lt;p&gt;If your &lt;code&gt;Dataset&lt;/code&gt; class looks something like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;class MyDataset(Dataset):
  
  # ... boilerplate ...

  def __getitem__(self, idx):
    item = self.data[idx]
    return item[&#39;anchor&#39;], item[&#39;positive&#39;], item[&#39;negative&#39;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;your collate function should be&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;def collate_fn(data):
    anchors, pos, neg = zip(*data)
    anchors = tokenizer(anchors, return_tensors=&amp;quot;pt&amp;quot;, padding=True)
    pos = tokenizer(pos, return_tensors=&amp;quot;pt&amp;quot;, padding=True)
    neg = tokenizer(neg, return_tensors=&amp;quot;pt&amp;quot;, padding=True)
    return anchors, pos, neg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and you can use it like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;dataset = MyDataset()
dataloader = DataLoader(dataset, 
                        batch_size=4, 
                        shuffle=True,
                        pin_memory=True,
                        collate_fn=collate_fn)

for anchors, positives, negatives in dataloader:
  anchors = anchors.to(device)
  positives = positives.to(device)
  negatives = negatives.to(device)
  
  # do more stuff
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;how-does-the-collate_fn-work&#34;&gt;How does the collate_fn work?&lt;/h2&gt;
&lt;figure&gt;&lt;img src=&#34;../img/torch_collate_fn.png&#34;
			alt=&#34;The PyTorch collate function accepts a list of results from calls to the dataset getitem function and combines their components into tensors for convenient training.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;The PyTorch collate function accepts a list of results from calls to the dataset &lt;strong&gt;getitem&lt;/strong&gt; function and combines their components into tensors for convenient training.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;</description>
            <content type="html"><![CDATA[<p>If your <code>Dataset</code> class looks something like</p>
<pre><code class="language-python">class MyDataset(Dataset):
  
  # ... boilerplate ...

  def __getitem__(self, idx):
    item = self.data[idx]
    return item['anchor'], item['positive'], item['negative']
</code></pre>
<p>your collate function should be</p>
<pre><code class="language-python">def collate_fn(data):
    anchors, pos, neg = zip(*data)
    anchors = tokenizer(anchors, return_tensors=&quot;pt&quot;, padding=True)
    pos = tokenizer(pos, return_tensors=&quot;pt&quot;, padding=True)
    neg = tokenizer(neg, return_tensors=&quot;pt&quot;, padding=True)
    return anchors, pos, neg
</code></pre>
<p>and you can use it like</p>
<pre><code class="language-python">dataset = MyDataset()
dataloader = DataLoader(dataset, 
                        batch_size=4, 
                        shuffle=True,
                        pin_memory=True,
                        collate_fn=collate_fn)

for anchors, positives, negatives in dataloader:
  anchors = anchors.to(device)
  positives = positives.to(device)
  negatives = negatives.to(device)
  
  # do more stuff
</code></pre>
<h2 id="how-does-the-collate_fn-work">How does the collate_fn work?</h2>
<figure><img src="/img/torch_collate_fn.png"
			alt="The PyTorch collate function accepts a list of results from calls to the dataset getitem function and combines their components into tensors for convenient training."><figcaption>
			<p>The PyTorch collate function accepts a list of results from calls to the dataset <strong>getitem</strong> function and combines their components into tensors for convenient training.</p>
		</figcaption>
</figure>

<p>Under the hood, your DataLoader is making multiple calls to <code>dataset.__getitem__()</code>, one for each item in <code>batch_size</code>. Usually it is smart enough to know how to combine them into a tensor for training (here is the <a href="https://pytorch.org/docs/stable/_modules/torch/utils/data/_utils/collate.html#default_collate">default collate function</a>), but sometimes you need custom logic.</p>
<p>Technically you don&rsquo;t need a collate function. You could collate within the training loop. But besides cluttering up your training loop, you won&rsquo;t be able to take advantage of multithreaded processing (<code>num_workers &gt; 1</code>), so your collation logic will block your training code.</p>
<h2 id="what-about-pin_memory">What about pin_memory?</h2>
<p>The <code>pin_memory</code> parameter specifies whether we should create the tensors in pinned RAM or not. This avoids some additional overhead when copying data to the GPU.</p>
<p>A common misconception is that this is pinning the GPU VRAM. Actually, this pins the RAM, <em>not</em> the VRAM.</p>
<p>If we check the <a href="https://developer.nvidia.com/blog/how-optimize-data-transfers-cuda-cc/">Nvidia technical blog</a> they give the following four recommendations (summarized):</p>
<ul>
<li>Minimize back and forth data transfers between RAM and GPU. Even if code is no faster on the GPU, it may be worth executing there to avoid the round-trip transfer cost, which is relatively slow.</li>
<li><strong>Use page-locked (&ldquo;pinned&rdquo;) memory when possible.</strong> By default, your machine uses pageable memory which means the physical address of RAM can change. When transferring data to the GPU, CUDA first needs to copy it to a fixed &ldquo;pinned&rdquo; address to be copied over. In your DataLoader, if you specify <code>pin_memory=True</code>, the tensors will automatically be created in pinned memory, avoiding the pageable memory -&gt; pinned memory copy.</li>
<li>Combine multiple small transfers into a large transfer to avoid overhead.</li>
<li>Data transfers can be executed asynchronously. In PyTorch that looks like <code>tensor.to(device, non_blocking=False)</code>.</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Very Large Datasets in PyTorch</title>
            <link>https://blog.lukesalamone.com/posts/very-large-datasets/</link>
            <pubDate>Thu, 27 Jun 2024 18:40:06 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/very-large-datasets/</guid>
            <description>&lt;blockquote&gt;
&lt;p&gt;In God we trust. All others must bring data. ~ W. Edwards Deming&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;datasets-that-fit-in-memory&#34;&gt;Datasets that fit in memory&lt;/h2&gt;
&lt;p&gt;For simple machine learning problems, your PyTorch dataset class probably looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;class SimpleDataset(Dataset):
    def __init__(self, features, targets):
        self.features = []
        for feature in features:
            self.features.append(self._feature_transform(feature))
        self.targets = targets

    def _feature_transform(self, feature):
        # Optional feature transformation function which 
        # converts each feature into its input representation 
        # for the model. This might be an expensive operation, 
        # so its best to do now rather than during training.
        return some_transformation_fn(feature)

    def __len__(self):
        return len(self.features)

    def __getitem__(self, idx):
        return self.features[idx], self.targets[idx]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this method, we basically load all of the data into RAM at once, which is perfectly fine for small datasets. But sooner or later you&amp;rsquo;re going to run into a machine learning problem with a large dataset. What do I mean by this? I mean a dataset which can&amp;rsquo;t easily fit into RAM/VRAM.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<blockquote>
<p>In God we trust. All others must bring data. ~ W. Edwards Deming</p>
</blockquote>
<h2 id="datasets-that-fit-in-memory">Datasets that fit in memory</h2>
<p>For simple machine learning problems, your PyTorch dataset class probably looks something like this:</p>
<pre><code class="language-python">class SimpleDataset(Dataset):
    def __init__(self, features, targets):
        self.features = []
        for feature in features:
            self.features.append(self._feature_transform(feature))
        self.targets = targets

    def _feature_transform(self, feature):
        # Optional feature transformation function which 
        # converts each feature into its input representation 
        # for the model. This might be an expensive operation, 
        # so its best to do now rather than during training.
        return some_transformation_fn(feature)

    def __len__(self):
        return len(self.features)

    def __getitem__(self, idx):
        return self.features[idx], self.targets[idx]
</code></pre>
<p>With this method, we basically load all of the data into RAM at once, which is perfectly fine for small datasets. But sooner or later you&rsquo;re going to run into a machine learning problem with a large dataset. What do I mean by this? I mean a dataset which can&rsquo;t easily fit into RAM/VRAM.</p>
<h2 id="datasets-that-fit-on-your-hard-drive">Datasets that fit on your hard drive</h2>
<p>There are ways around this issue. One is using memory mapping. The idea is, rather than loading all of the data into memory, let&rsquo;s store it on the disk and seek to it during training. This also means we can store the transformed tensors on the disk and load them as needed, rather than recomputing them every time we load data from disk.</p>
<p>Now we have a two step approach. The first step is transforming the features and targets to a memory-mapped file, and the second is loading it on-demand. For the first step we will create a list of memmap files, and for the second step we will use <code>dask</code> to virtually concatenate them together and read them as if they were a contiguous array.</p>
<pre><code class="language-python">import numpy as np
import uuid
import os

# store all features in memmap files
# you can do the same thing with targets

features_dir = 'features/'

for feature_file in feature_files:
    with open(feature_file) as f:
        features = transform_features(f.read())
    
    fname = f'{uuid.uuid4()}.memmap'

    # create the file on disk
    memmap_file = np.memmap(os.path.join(features_dir, fname), 
                            dtype='float64', 
                            mode='w+', 
                            shape=features.shape)

    # write features to memmap array
    memmap_file[:,:] = features

    # write changes to disk
    memmap_file.flush()
    

# Load the data from memmaps. We can use dask to create a 
# virtual contiguous array for simplicity.

import dask.array as da

class MemmapDataset(Dataset):
    def __init__(self, 
            feature_files, 
            target_files, 
            features_shape, 
            targets_shape):
        self.features = []
        self.targets = []
        
        for file in feature_files:
            self.features.append(np.memmap(file, 
                                           dtype='float64', 
                                           mode='r', 
                                           shape=features_shape))
        for file in target_files:
            self.targets.append(np.memmap(file, 
                                          dtype='float64', 
                                          mode='r', 
                                          shape=targets_shape))

        self.features = da.concatenate(self.features, axis=0)
        self.targets = da.concatenate(self.targets, axis=0)

    def __len__(self):
        return self.features.shape[0]

    def __getitem__(self, idx):
        return (
            torch.tensor(self.features[idx]), 
            torch.tensor(self.targets[idx])
        )
</code></pre>
<h2 id="sparse-vectors">Sparse vectors</h2>
<p>For very sparse vectors, where most of the items are zero, my recommendation is to store the nonzero indices of each vector rather than the whole thing. This is obviously an empirical determination, but it can require significantly less space to store the indices and build at train time than to store the entire vector. You can use a custom collate function to expand the training data during training:</p>
<pre><code class="language-python">def collate_fn(data):
    '''
    Here, data is a list of tuples returned from dataset[idx] 
    above. But supposing that &quot;features&quot; actually corresponds 
    to the nonzero indices of the features, we can expand the 
    tensor for training.

    This example is for a model which expects input dimension
    (bs, 1024) but you can adapt for your needs.
    '''
    feature_idxs, targets = zip(*data)
    bs = len(features)
    features = torch.zeros((bs * 1024))
    features[feature_idxs] = 1.0
    return features.view((bs, 1024)), targets
</code></pre>
<p>You then use the collate function like</p>
<pre><code class="language-python">dataset = MemmapDataset(...) # or SimpleDataset
dataloader = DataLoader(dataset, 
                        collate_fn=collate_fn, 
                        batch_size=bs)
</code></pre>
<h2 id="datasets-that-dont-fit-on-your-hard-drive">Datasets that don&rsquo;t fit on your hard drive</h2>
<p>Sometimes the dataset is so big that it doesn&rsquo;t fit on your hard drive. In that case, you might need to reorganize your code a bit, preprocessing your data into unique UUID-based shard files (<code>sid</code>s), and training each <code>sid</code> one by one.</p>
<pre><code class="language-python">sids = [
    # you may have thousands of shard ids here
    '5c91ad9e-4963-4dfa-8885-6a351dd9fbb8',
    '8ecb542f-00df-468e-8f89-34a6608648d6',
    'f4536d05-a8ff-4591-9322-10860cd06942'
]

for sid in sids:
    dataset = ShardedDataset(sid)
    dataloader = DataLoader(dataset, batch_size=64, shuffle=True)

    for i, batch in enumerate(dataloader):
        # do your training
        pass
</code></pre>
<p>And your <code>Dataset</code> class will look something like</p>
<pre><code class="language-python">class VeryLargeDataset(Dataset):
    def __init__(self, sid):
        self.data = self._load_shard(sid)

    def _load_shard(self, sid):
        # load from your hard drive or remote
        pass

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]
</code></pre>
<p>Despite the reorganization required, this is a very flexible approach and can scale to an effectively infinite amount of data. For continuous training, this is often a very effective approach.</p>
<h2 id="bonus-use-rustc-python-bindings">Bonus: Use Rust/C Python bindings</h2>
<p>Your feature transformation might be pretty slow depending on what you need to do. In my experience I was waiting 10 minutes to process text files in Python, which really slows down the development/training process. A much more effective approach can be to use Python bindings from a faster language, such as Rust or C.</p>
<p>Rust can even return native numpy arrays, making data transformations very efficient. For more on setting up Rust bindings, <a href="/posts/how-to-create-rust-python-bindings/">check out my how-to article</a>.</p>
<pre><code class="language-python">import my_bindings

files_to_process = [...]
data = []
for file in files_to_process:
    data.append(my_bindings.process_file(file))
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>How to Create Rust Python Bindings</title>
            <link>https://blog.lukesalamone.com/posts/how-to-create-rust-python-bindings/</link>
            <pubDate>Tue, 18 Jun 2024 16:21:35 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/how-to-create-rust-python-bindings/</guid>
            <description>&lt;p&gt;Rust is super fast. Python is super flexible. Porting slow python code to rust can make your life a lot easier, and it&amp;rsquo;s not too difficult to set up.&lt;/p&gt;
&lt;p&gt;I will demonstrate rust bindings for summing the integers in a large text file containing a billion digits that looks like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;6,9,8,3,0,1,8,4,9,7,6,3,4,2,6,0,0,5,1,1, . . . ,4,5,9,3,3,2,8,3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;general-steps&#34;&gt;General steps&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;../posts/how-to-create-rust-python-bindings/#install-rust-and-maturin&#34;&gt;install rust and maturin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;../posts/how-to-create-rust-python-bindings/#set-up-boilerplate&#34;&gt;set up boilerplate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;../posts/how-to-create-rust-python-bindings/#add-your-function&#34;&gt;add your function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;../posts/how-to-create-rust-python-bindings/#compile-and-import&#34;&gt;compile and import&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;install-rust-and-maturin&#34;&gt;Install Rust and Maturin&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;curl --proto &#39;=https&#39; --tlsv1.2 -sSf https://sh.rustup.rs | sh
pip install maturin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See also &lt;a href=&#34;https://www.rust-lang.org/tools/install&#34;&gt;install rust&lt;/a&gt; and &lt;a href=&#34;https://www.maturin.rs/installation&#34;&gt;install maturin&lt;/a&gt;&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Rust is super fast. Python is super flexible. Porting slow python code to rust can make your life a lot easier, and it&rsquo;s not too difficult to set up.</p>
<p>I will demonstrate rust bindings for summing the integers in a large text file containing a billion digits that looks like</p>
<pre><code class="language-text">6,9,8,3,0,1,8,4,9,7,6,3,4,2,6,0,0,5,1,1, . . . ,4,5,9,3,3,2,8,3
</code></pre>
<h2 id="general-steps">General steps</h2>
<ol>
<li><a href="/posts/how-to-create-rust-python-bindings/#install-rust-and-maturin">install rust and maturin</a></li>
<li><a href="/posts/how-to-create-rust-python-bindings/#set-up-boilerplate">set up boilerplate</a></li>
<li><a href="/posts/how-to-create-rust-python-bindings/#add-your-function">add your function</a></li>
<li><a href="/posts/how-to-create-rust-python-bindings/#compile-and-import">compile and import</a></li>
</ol>
<h2 id="install-rust-and-maturin">Install Rust and Maturin</h2>
<pre><code class="language-bash">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
pip install maturin
</code></pre>
<p>See also <a href="https://www.rust-lang.org/tools/install">install rust</a> and <a href="https://www.maturin.rs/installation">install maturin</a></p>
<h2 id="set-up-boilerplate">Set up boilerplate</h2>
<pre><code class="language-bash"># select &quot;pyo3&quot; here
maturin new cool_rust_bindings
cd cool_rust_bindings
</code></pre>
<h2 id="add-your-function">Add your function</h2>
<p>Open <code>src/lib.rs</code> and change it to the following:</p>
<pre><code class="language-rust">use pyo3::prelude::*;
use std::fs::File;
use std::io::{self, Read};

#[pyfunction]
fn sum_file(filename:&amp;str) -&gt; io::Result&lt;i64&gt; {
    let mut file = File::open(filename)?;
    let mut contents = String::new();
    file.read_to_string(&amp;mut contents)?;

    let mut total:i64 = 0;
    for x in contents.chars() {
        if x != ','{
            total += (x as i64) - 48;
        }
    }

    Ok(total)
}

#[pymodule]
fn cool_rust_bindings(_py: Python, m: &amp;PyModule) -&gt; PyResult&lt;()&gt; {
    m.add_function(wrap_pyfunction!(sum_file, m)?)?;
    Ok(())
}

</code></pre>
<p>Here we&rsquo;re just creating a simple function that sums the integers in a file. The rust code gave a 3x speedup over the corresponding python code. Depending on the task, I&rsquo;ve gotten 10-20x speedups.</p>
<h2 id="compile-and-import">Compile and import</h2>
<p>Run the following from the <code>cool_rust_bindings</code> directory</p>
<pre><code class="language-bash">maturin develop
</code></pre>
<p>This will add <code>cool_rust_bindings</code> to your python path. Use it like</p>
<pre><code class="language-python">import cool_rust_bindings

cool_rust_bindings.sum_file('large_file.csv')
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>The Most Ramified Chess Position of 2023</title>
            <link>https://blog.lukesalamone.com/posts/most-ramified-chess-position-2023/</link>
            <pubDate>Thu, 13 Jun 2024 19:38:39 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/most-ramified-chess-position-2023/</guid>
            <description>&lt;script src=&#34;https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js&#34;&gt;&lt;/script&gt;
&lt;p&gt;&lt;strong&gt;I spent some time considering words to describe a chess position with many legal moves. &amp;ldquo;Complex&amp;rdquo; doesn&amp;rsquo;t quite capture the situation since we would usually describe a complex position as one with many tactical interations. &lt;a href=&#34;https://en.wikipedia.org/wiki/Ramification_%28botany%29&#34;&gt;Ramified&lt;/a&gt; seems to make the most sense, as it describes &amp;ldquo;branching out&amp;rdquo;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The opening position in chess has 20 legal moves. From there, the number of legal moves in a position tends to increase as pieces move towards the center of the board, before decreasing as the number of pieces on the board drops in the endgame.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<p><strong>I spent some time considering words to describe a chess position with many legal moves. &ldquo;Complex&rdquo; doesn&rsquo;t quite capture the situation since we would usually describe a complex position as one with many tactical interations. <a href="https://en.wikipedia.org/wiki/Ramification_%28botany%29">Ramified</a> seems to make the most sense, as it describes &ldquo;branching out&rdquo;.</strong></p>
<p>The opening position in chess has 20 legal moves. From there, the number of legal moves in a position tends to increase as pieces move towards the center of the board, before decreasing as the number of pieces on the board drops in the endgame.</p>
<p>People have composed <a href="https://lichess.org/analysis/fromPosition/R6R/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q2/pp1Q4/kBNN1KB1_w_-_-">artificial positions</a> with a huge number of legal moves. The most ramified artificial position I have found has 218 legal moves. But it&rsquo;s hard to imagine how such a position could come about in a real game, which is what I&rsquo;m interested in. So I churned through all of the positions in all of the Lichess games which took place in 2023 in search of the most ramified chess position. Here is what I found.</p>
<h2 id="most-games-max-out-around-16-moves-in">Most games max out around 16 moves in</h2>
<p>In the chart below you can see the maximum moves at any point in the game. The x-axis represents half moves. (In other words, if white makes a move and black makes a move, that&rsquo;s two half moves.) Each game starts out with exactly 20 possible moves for white, which quickly increases in the middle game before gradually decreasing again.</p>
<p><canvas id="movesByPly" width="800" height="600"></canvas></p>
<p>The colors represent the percentiles, since at every slice along the x-axis there is actually a distribution of positions and associated legal moves. Interestingly, we can see that for the 10th percentile the bottom really falls out after 40 moves or so, with positions so constrained that there are only a handful of legal moves. These likely represent positions where the king is in check.</p>
<h2 id="100-move-positions-are-rare">100+ move positions are rare</h2>
<figure><img src="/img/pgn_processing.gif"
			alt="Each line represents the chess position in a chunk of 5000 games with the most legal moves."><figcaption>
			<p>Each line represents the chess position in a chunk of 5000 games with the most legal moves.</p>
		</figcaption>
</figure>

<p>To parse the games, I wrote a multithreaded rust script with python bindings to process chunks of 5000 games at a time. Within a chunk of games, it was pretty typical for the maximum number of legal moves in any position in any game in the chunk to be between 70 and 100.</p>
<p>Here is a typical position with 84 legal moves for white:</p>
<iframe src="https://lichess.org/embed/game/bDB6sSJc?theme=auto&bg=auto#92"
width=760 height=600 frameborder=0></iframe>
<h2 id="very-few-ever-exceed-120">Very few ever exceed 120</h2>
<p>When you get past 100, most of the positions have several queens. There are exceptions, however. <a href="https://lichess.org/paqlYvUW#177">This 112 move gem has zero</a>.</p>
<p>Here is a typical 120-move position with six queens:</p>
<iframe src="https://lichess.org/embed/game/aWnIfauG?theme=auto&bg=auto#155"
width=760 height=600 frameborder=0></iframe>
<h2 id="the-147-move-position-">The 147 move position 🤯</h2>
<p>Here it is, the 147 legal move masterpiece.</p>
<iframe src="https://lichess.org/embed/game/cJq0LRav?theme=auto&bg=auto#117"
width=760 height=600 frameborder=0></iframe>
<h2 id="runners-up">Runners up</h2>
<ul>
<li><a href="https://lichess.org/MvZ3mrd5#153">8 queens, 146 legal moves</a></li>
<li><a href="https://lichess.org/h0UOgkCk#155">9 queens, 144 legal moves</a></li>
<li><a href="https://lichess.org/NvNWFSg4#96">9 queens, 142 legal moves</a></li>
<li><a href="https://lichess.org/lcPavnZp#153">8 queens, 142 legal moves</a></li>
</ul>
<script>
  let percentiles = {
    "10": [20, 21, 22, 24, 26, 26, 26, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 25, 24, 24, 24, 23, 22, 22, 21, 20, 20, 19, 18, 16, 14, 12, 11, 9, 8, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3], 
    "20": [20, 24, 24, 25, 28, 28, 29, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29, 28, 28, 28, 27, 26, 26, 26, 25, 24, 24, 24, 23, 22, 22, 21, 20, 20, 19, 18, 18, 17, 16, 16, 16, 15, 14, 13, 12, 12, 12, 11, 10, 10, 10, 9, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5], 
    "30": [20, 24, 25, 25, 28, 30, 30, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, 32, 32, 32, 32, 32, 31, 30, 30, 30, 30, 29, 28, 28, 28, 27, 26, 26, 25, 24, 24, 23, 22, 22, 22, 21, 20, 20, 20, 19, 18, 18, 18, 17, 16, 16, 16, 15, 14, 14, 14, 14, 13, 12, 12, 12, 12, 11, 11, 10, 10, 10, 10, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 7], 
    "40": [20, 24, 28, 28, 29, 31, 32, 32, 33, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 32, 32, 32, 32, 31, 30, 30, 29, 28, 28, 28, 27, 26, 26, 26, 25, 24, 24, 23, 22, 22, 22, 21, 20, 20, 20, 19, 18, 18, 18, 18, 17, 17, 16, 16, 16, 16, 15, 15, 14, 14, 14, 14, 14, 13, 13, 13, 12, 12, 12, 12, 12, 11, 11, 11], 
    "50": [20, 24, 28, 29, 30, 32, 33, 34, 35, 36, 36, 36, 36, 36, 36, 36, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 37, 37, 37, 36, 36, 36, 36, 35, 34, 34, 34, 33, 32, 32, 32, 31, 30, 30, 30, 29, 28, 28, 28, 27, 26, 26, 26, 25, 24, 24, 24, 22, 22, 22, 22, 21, 20, 20, 20, 19, 19, 18, 18, 18, 18, 18, 17, 17, 17, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15, 15, 14], 
    "60": [20, 24, 29, 30, 31, 33, 34, 35, 36, 36, 37, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 39, 38, 38, 38, 38, 38, 38, 37, 36, 36, 36, 35, 34, 34, 34, 33, 32, 32, 32, 31, 30, 30, 30, 29, 28, 28, 28, 27, 26, 26, 26, 25, 24, 24, 24, 23, 22, 22, 22, 22, 21, 21, 20, 20, 20, 20, 20, 19, 19, 19, 18, 18, 18, 18, 18, 18, 18, 18, 17], 
    "70": [20, 25, 30, 30, 32, 34, 35, 36, 38, 38, 39, 40, 40, 40, 40, 40, 41, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 43, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 41, 40, 40, 40, 40, 40, 39, 38, 38, 38, 38, 37, 36, 36, 36, 36, 35, 34, 34, 33, 32, 32, 31, 30, 30, 30, 29, 29, 28, 28, 28, 27, 26, 26, 26, 26, 25, 24, 24, 24, 24, 23, 23, 22, 22, 22, 22, 22, 21, 21, 21, 21, 21, 20, 20, 20], 
    "80": [20, 25, 30, 32, 34, 36, 37, 38, 39, 40, 41, 42, 42, 42, 42, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 44, 44, 44, 44, 44, 44, 44, 44, 43, 42, 42, 42, 42, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 36, 36, 36, 35, 34, 34, 34, 33, 33, 32, 32, 32, 31, 30, 30, 30, 30, 29, 28, 28, 28, 28, 27, 27, 26, 26, 26, 26, 26, 26, 25, 25, 25, 25, 25, 24, 24], 
    "90": [20, 25, 30, 34, 36, 37, 38, 40, 41, 42, 43, 44, 45, 46, 46, 46, 46, 46, 47, 48, 48, 48, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 47, 46, 46, 46, 46, 46, 46, 46, 45, 44, 44, 44, 43, 42, 42, 42, 42, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 36, 36, 36, 36, 35, 34, 34, 34, 34, 33, 33, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 30, 30, 30]
    };

  let colors = ['#00ff0019', '#00ff1c4c', '#00ff3980', '#00ff55b3', '#00ff71e6', '#00ff8eb3', '#00ffaa80', '#00ffc64c', '#00ffe319'];

  let datasets = Object.keys(percentiles).map(x => parseInt(x)).sort((a, b) => a-b).map((key, idx) => {
    return {
      label: `${key}th percentile`,
      data:percentiles[key + ''],
      backgroundColor: colors[idx],
      borderColor: colors[idx].slice(0,7),
      borderWidth: 1,
      pointRadius: 0,
      fill:'+1'
    };
  })

  const config = {
    type: 'line',
    data: {
      labels:Array.from({length: 101}, (_, index) => index),
      datasets: datasets
    },
    options: {
      scales: {
        y: {
          beginAtZero: true,
          title: {
            display: true,
            text: '# legal moves'
          }
        },
        x: {
          title: {
            display: true,
            text: 'half-moves into game'
          }
        }
      },
      plugins: {
        title: {
          display: true,
          text: 'number of moves by ply',
          font: {
            size: 16
          },
          padding: {
            top: 10,
            bottom: 30
          }
        },
        // legend: {
        //   display: false
        // }
      }
    }
  };

  const myChart = new Chart(document.getElementById('movesByPly'), config);
</script>
<script>
  let histData = ["0.16264871", "0.00656638", "0.00871243", "0.00130045", "0.00086964", "0.00038673", "0.00075342", "0.02417359", "0.03599588", "0.08046975", "0.12240082", "0.04151829", "0.06883581", "0.05604168", "0.06971947", "0.08875537", "0.09150055", "0.11839126", "0.19631219", "0.31351120", "0.44738365", "0.69643930", "1.02187499", "1.54113618", "2.12636978", "2.79879780", "3.68506507", "4.60544467", "5.53388347", "6.45800011", "7.69917063", "7.53643576", "7.84121249", "7.36049398", "6.88992060", "6.26871116", "5.44162148", "4.60481949", "3.80167096", "3.03858799", "2.38255688", "1.83647712", "1.38158531", "1.02173072", "0.74028999", "0.52558111", "0.37072709", "0.25889421", "0.17859478", "0.12151114", "0.08456747", "0.05826788", "0.03935822", "0.02706504", "0.01928837", "0.01363772", "0.01005095", "0.00762638", "0.00556449", "0.00434419", "0.00359678", "0.00307780", "0.00240053", "0.00204987", "0.00173327", "0.00146677", "0.00138661", "0.00123633", "0.00114416", "0.00100389", "0.00084359", "0.00075142", "0.00065123", "0.00055905", "0.00054904", "0.00049493", "0.00041478", "0.00035667", "0.00037070", "0.00029856", "0.00024847"].map(x => parseFloat(x));


const config2 = {
    type: 'line',
    data: {
      labels:Array.from({length: 81}, (_, index) => index+20),
      datasets: [{
          label: `max moves`,
          data:histData,
          backgroundColor: '#ff00ff22',
          borderColor: '#ff00ff',
          borderWidth: 1,
          pointRadius: 1,
          fill:'origin'
        }]
    },
    options: {
      scales: {
        y: {
          beginAtZero: true,
          title: {
            display: true,
            text: '% of games'
          }
        },
        x: {
          title: {
            display: true,
            text: 'max moves in the game'
          }
        }
      },
      plugins: {
        tooltip: {
                enabled: true,
                callbacks: {
                    title: function(context) {
                      let pct = context[0].parsed.y.toFixed(2);
                      let count = context[0].label;

                      return `${pct}% of games had a position with ${count} legal moves`;
                    },
                    label: function(context) {
                        return null
                    },
                }
            },
        title: {
          display: true,
          text: 'max moves by game',
          font: {
            size: 16
          },
          padding: {
            top: 10,
            bottom: 30
          }
        },
        legend: {
          display: false
        }
      }
    }
  };

new Chart(document.getElementById('maxMovesHist'), config2);
</script>
]]></content>
        </item>
        
        <item>
            <title>What are Sparse Autoencoders?</title>
            <link>https://blog.lukesalamone.com/posts/sparse-autoencoder/</link>
            <pubDate>Thu, 06 Jun 2024 16:30:27 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/sparse-autoencoder/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    // Fix &lt;code&gt; tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;p&gt;&lt;strong&gt;TLDR: A sparse autoencoder is just a regular autoencoder that encourages sparsity with an L1 penalty or KL divergence loss rather than using a low-dimensional bottleneck.&lt;/strong&gt;&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    // Fix <code> tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<p><strong>TLDR: A sparse autoencoder is just a regular autoencoder that encourages sparsity with an L1 penalty or KL divergence loss rather than using a low-dimensional bottleneck.</strong></p>
<p>If you understood all of those words above, you may be interested in the <a href="https://cdn.openai.com/papers/sparse-autoencoders.pdf">OpenAI paper</a> which used sparse autoencoders to interpret features from GPT-4.</p>
<p>If not, I&rsquo;ll try to break it down.</p>
<h2 id="what-is-an-autoencoder">What is an autoencoder?</h2>
<p>An autoencoder is a machine learning architecture which contains two functions: an <code>encoder</code> and a <code>decoder</code>. The <code>encoder</code> learns to create an efficient representation of the input, similar to lossy file compression. The <code>decoder</code> learns to reconstruct the original input from the efficient representation.</p>
<p>I have a couple of other pretty good explanations on this. In the first post, I described <a href="/posts/build-an-autoencoder/">how to build an autoencoder</a>. Later, I discussed their potential <a href="/posts/learning-the-haystack/#autoencoders">applications in vector retrieval</a>.</p>
<h2 id="what-is-sparsity">What is sparsity?</h2>
<p>Sparsity just means that most of the values in a vector are zero.</p>
<h2 id="what-is-an-l1-penalty">What is an L1 penalty?</h2>
<p>L1 stands for penalizing the $ L^1 $ norm of a vector. In other words, the sum of the absolute values of the values in the vector:</p>
<p style="text-align: center; font-size: 1.5em">
$$
\text{L1 norm} = \sum_{i=1}^N \vert x_i \vert
$$
</p>
<h2 id="what-is-kl-divergence">What is KL divergence?</h2>
<p><a href="https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence">KL divergence</a> is a measure of the &ldquo;distance&rdquo; one probability distribution has from another:</p>
<p style="text-align: center; font-size: 1.5em">
$$
\sum_{x \in X} P(x) \log \left( \frac{P(x)}{Q(x)} \right)
$$
</p>
<p>Note that KL divergence is not symmetrical, so distance isn&rsquo;t an entirely accurate description. If you&rsquo;re looking for a symmetrical measure of distribution similarity, use Jensen-Shannon divergence. Additionally, $ X $ doesn&rsquo;t have to be a continuous variable, although it often is.</p>
<p>In the demo below you can see that as the probability distributions overlap more and more, the KL divergence decreases.</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>
<script src="/js/kl_divergence.js"></script>
<p><canvas id="myChart" style="background-color: #0000"></canvas></p>
<div style="text-align:center">
  <span>mean:
  <input type="range" min="300" max="600" value="400" class="slider" id="mean">
  <span id="mean_label">40.0</span>
</div>
<div style="text-align:center">
  <span>variance:
  <input type="range" min="100" max="500" value="200" class="slider" id="variance">
  <span id="variance_label">20.0</span>
</div>
<div style="text-align:center">
  <span id="kl_divergence_label"></span>
</div>
<h2 id="what-is-a-kl-divergence-loss">What is a KL divergence loss?</h2>
<p><a href="https://pytorch.org/docs/stable/generated/torch.nn.KLDivLoss.html">KL divergence loss</a> tries to force a model&rsquo;s predicted distribution to match a target distribution. Common applications of KL divergence loss are t-SNE dimensionality reduction and generative models like variational autoencoders and generative adversarial networks.</p>
]]></content>
        </item>
        
        <item>
            <title>How does HNSW work?</title>
            <link>https://blog.lukesalamone.com/posts/how-does-hnsw-work/</link>
            <pubDate>Mon, 20 May 2024 13:38:01 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/how-does-hnsw-work/</guid>
            <description>&lt;p&gt;Suppose we have a vector database with a billion &lt;a href=&#34;../posts/learning-the-haystack/&#34;&gt;embeddings&lt;/a&gt; in it (the &lt;em&gt;haystack&lt;/em&gt;). And suppose we are looking for K vectors, the &lt;em&gt;needles&lt;/em&gt; which maximize some similarity function. (In the case of cosine similarity or euclidean distance, we may be maximizing &lt;code&gt;1-distance(x,y)&lt;/code&gt;.) And also suppose that we&amp;rsquo;d like to do this quickly.&lt;/p&gt;
&lt;h2 id=&#34;naive-and-semi-naive-approaches&#34;&gt;Naive and semi-naive approaches&lt;/h2&gt;
&lt;p&gt;One approach might be to compare every vector and take the argmax. In that case, for vectors of length D our runtime will be &lt;code&gt;1 billion x D&lt;/code&gt;.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Suppose we have a vector database with a billion <a href="/posts/learning-the-haystack/">embeddings</a> in it (the <em>haystack</em>). And suppose we are looking for K vectors, the <em>needles</em> which maximize some similarity function. (In the case of cosine similarity or euclidean distance, we may be maximizing <code>1-distance(x,y)</code>.) And also suppose that we&rsquo;d like to do this quickly.</p>
<h2 id="naive-and-semi-naive-approaches">Naive and semi-naive approaches</h2>
<p>One approach might be to compare every vector and take the argmax. In that case, for vectors of length D our runtime will be <code>1 billion x D</code>.</p>
<pre><code class="language-python">def search(needle, haystack, arg_k=1):
    # normalize needle and haystack vectors
    needle /= np.linalg.norm(needle)
    haystack /= np.linalg.norm(haystack, axis=1, keepdims=True)

    # compute cosine similarities
    similarities = np.dot(haystack, needle)

    # get the indexes of the top k elements
    idx_topk = np.argpartition(a, -k)[-k:]

    # return haystack vector with highest cosine similarity
    return haystack[idx_topk]
</code></pre>
<p>In practice, there are some optimizations we can try which can increase the practical speed of this operation:</p>
<ul>
<li>We can parallelize computations on-device with <a href="https://en.wikipedia.org/wiki/Single_instruction,_multiple_data">SIMD</a> or <a href="https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms">BLAS</a>.</li>
<li>We can parallelize computations on-device with GPU.</li>
<li>We can parallelize across cores for machines with multiple cores.</li>
<li>We can parallelize across devices. For example, partitioning the database across 10 clusters, finding a &ldquo;winner&rdquo; from each cluster, and then repeating the search across the 10 winners.</li>
</ul>
<p>There are also some relatively simple but &ldquo;destructive&rdquo; techniques we can also try:</p>
<ul>
<li>We can try pruning the dataset in some way beforehand. For example, maybe we don&rsquo;t need to consider old or unpopular vectors.</li>
<li>We can try reducing the dimensionality of the vectors with something like PCA.</li>
<li>We can try quantizing the vectors e.g. <code>f64</code> -&gt; <code>f16</code> reduces memory requirements 4x. (Usually this requires quantization-aware training.)</li>
</ul>
<h2 id="hierarchical-navigable-small-worlds-hnsw">Hierarchical Navigable Small Worlds (HNSW)</h2>
<p>Sometimes linear time is still too slow. In that case, we can consider an <em>approximate</em> solution, where some acceptable percent of vectors returned are actually not in the top K nearest neighbors. This is called approximate nearest neighbors (ANN). If we have a large system, it&rsquo;s possible to <a href="https://sbert.net/examples/applications/retrieve_rerank/">use another more precise model to rerank</a> the vectors results anyways. In that case, we just need to set K large enough to reliably include the vectors we really need.</p>
<p>ANN algorithms typically come in three flavors: graph based approaches, space partitioning based approaches, and hash based approaches. If your dataset is large enough, you might add compression (e.g. product quantization) on top of it.</p>
<h3 id="intuition">Intuition</h3>
<p><a href="https://arxiv.org/pdf/1603.09320">HNSW</a> is a bit of a Six Degrees of Kevin Bacon approach. I&rsquo;ll give what I think this is a fairly good intuition for how it works.</p>
<p>Suppose you have 1,000 friends and you&rsquo;d like to know which five of them live closest to a landmark like the Golden Gate Bridge. You don’t have everyone&rsquo;s exact address, but each friend keeps a list of their close friends&rsquo; addresses. You might start with a friend who lives in the US. From there, you check their friends to find who lives closest to the target, and then search among their friends for someone even closer. By using a hierarchical approach, you don&rsquo;t need to search through everyone, just those likely to be closest.</p>
<p>In the analogy, the Golden Gate Bridge is the &ldquo;needle&rdquo; and the friends are the &ldquo;haystack&rdquo;. This is a pretty efficient method, but of course it is possible to miss a node if their friend wasn&rsquo;t linked to an earlier friend.</p>
<figure><img src="/img/hnsw.png"
			alt="HNSW uses a multi-layered graph for efficient nearest neighbor search."><figcaption>
			<p>HNSW uses a multi-layered graph for efficient nearest neighbor search.</p>
		</figcaption>
</figure>

<h3 id="searching">Searching</h3>
<p>Searching is fairly straightforward. Start from an entry point (maybe a random point) in the top layer. For each of the neighbors of the entry point, compute the distance between the query and that point. If any of the neighbors are closer to the query, hop to that neighbor and repeat the process. Select the closest point and go to the next layer down. We keep searching until we reach layer 0.</p>
<p>To compute the K nearest neighbors during search (with K &gt; 1) we simply maintain a min heap, and add all nodes considered during the search. The number of nodes returned in this search (K) is also known as <code>ef_search</code> in the HNSW paper.</p>
<h3 id="addition">Addition</h3>
<p>To add a vector X, we start by computing the highest layer <code>L</code> the node will appear in. <code>L</code> is randomly selected using an exponentially decaying probability. (All nodes appear in level 0, but can have skip links to higher levels.) Next, starting from the top layer, we traverse the graph structure as if we were searching for X.</p>
<p>Once we reach layer <code>L</code> from earlier, we begin to make connections. Connections can be made with previously found nearest neighbors or nearest neighbors on the current layer. The number of nearest neighbors to consider connections with is controlled by the <code>ef_construction</code> parameter. The number of links to actually create is controlled by the parameter <code>M</code>.</p>
<h4 id="performance-considerations">Performance considerations</h4>
<ul>
<li>HNSW can require a large amount of memory. This can be reduced with product quantization, a lossy vector compression method.</li>
<li>Adding new elements is slow. There is a tradeoff between index quality and time required to build the index controlled by <code>ef_construction</code>.</li>
</ul>
<p>More information about HNSW parameters can be found <a href="https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md">here</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Learning the Haystack</title>
            <link>https://blog.lukesalamone.com/posts/learning-the-haystack/</link>
            <pubDate>Wed, 27 Mar 2024 18:19:54 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/learning-the-haystack/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    // Fix &lt;code&gt; tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;p&gt;Embeddings, or vector representations of a document (which could be a piece of text, image, sound, etc.), can be extremely useful for making sense of large datasets. They transform information into a vector space such that their distance corresponds to their similarity.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    // Fix <code> tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<p>Embeddings, or vector representations of a document (which could be a piece of text, image, sound, etc.), can be extremely useful for making sense of large datasets. They transform information into a vector space such that their distance corresponds to their similarity.</p>
<p>Enterprising readers might be asking themselves how to get these vectors (also known as embeddings) in the first place. One way is to simply pay for them. This isn&rsquo;t ideal for a couple of reasons:</p>
<ol>
<li>It can be expensive. You&rsquo;ll need to pay once to embed each document, and separately for each query. If you have a large corpus, it can be cost-prohibitive. At the time of writing, OpenAI charges about $130 per million tokens (around 1000 paragraphs) for their largest model.</li>
<li>&ldquo;Similarity&rdquo; may mean different things depending on your use case. For example, suppose we are retrieving documents for customer support. For the embedding model to learn that two documents share user behavior characteristics (i.e. two documents were opened by support agents in the same session), that information needs to be available in the training process.</li>
</ol>
<p>Below is an overview of three of the main training regimes I have used for creating embeddings. For more information and in-depth examples, I highly recommend the loss overview page of the <a href="https://sbert.net/docs/sentence_transformer/loss_overview.html">Sentence Transformers</a> library. Generally speaking, there are three categories of methods: unsupervised methods, contrastive learning methods (positive/negative labels), and regression methods (floating-point labels).</p>
<h2 id="autoencoders">Autoencoders</h2>
<figure><img src="/img/mona_lisa_autoencoder.png"
			alt="An autoencoder uses a bottleneck to reduce the dimensionality of the input."><figcaption>
			<p>An autoencoder uses a bottleneck to reduce the dimensionality of the input.</p>
		</figcaption>
</figure>

<p>One approach for vectorizing a document, image, or other blob of information is to simply <a href="/posts/build-an-autoencoder/">use an autoencoder</a>. An autoencoder is a function which learns a lossy compression function. It can be considered an unsupervised method since each item is its own label.</p>
<p>However, although they are conceptually pretty simple, autoencoders aren&rsquo;t always best. They may learn an efficient representation of your document, but it may be wasting a lot of space on things you don&rsquo;t care about.</p>
<p>For example, if it was reproducing a picture which contained a TV showing static, you may not care exactly what the static <em>looks like</em>, just that it <em>has</em> static. So it&rsquo;s probably a waste of space to try to reproduce every pixel of static. Likewise, if it were encoding people, it might be really important to you that the people have 5 fingers. Unfortunately, the autoencoder wasted too much space encoding the TV and not on boring details like numbers of fingers.</p>
<p>See also: <a href="https://www.youtube.com/watch?v=UABxQ19Sqt0">the &ldquo;noisy TV problem&rdquo;</a></p>
<h2 id="fine-tuning-with-sentence-transformers">Fine-tuning with Sentence Transformers</h2>
<p>The applicability of the next few methods will depend on the format of your data and labels.</p>
<p>Here is a simple example for creating text embeddings with <code>sentence-transformers</code> using ContrastiveLoss:</p>
<pre><code class="language-python">from sentence_transformers import (
  SentenceTransformer,
  SentenceTransformerTrainer
)
from sentence_transformers.losses import ContrastiveLoss
from datasets import Dataset

model = SentenceTransformer('all-MiniLM-L6-v2')
train_dataset = Dataset.from_dict({
    &quot;sentence1&quot;: [
      &quot;I feel the need...&quot;,
      &quot;Life is like a box of chocolates.&quot;
    ],
    &quot;sentence2&quot;: [
      &quot;...the need for speed!&quot;, # similar
      &quot;Here's Johnny!&quot; # dissimilar
    ],
    &quot;label&quot;: [1, 0],
})

trainer = SentenceTransformerTrainer(
    model=model,
    train_dataset=train_dataset,
    loss=ContrastiveLoss(model),
)
trainer.train()
</code></pre>
<p>And to generate the embedding:</p>
<pre><code class="language-python">embedding = model.encode([&quot;There's no crying in baseball!&quot;])
print(embedding.shape) # (1, 384)
</code></pre>
<h2 id="contrastive-learning-triplet-loss">Contrastive Learning: Triplet loss</h2>
<p><a href="https://pytorch.org/docs/stable/generated/torch.nn.TripletMarginWithDistanceLoss.html">Triplet loss</a> explicitly learns embeddings to be used in cosine similarity or euclidean distance (L2 norm) comparison. It uses triplets of the form (A, P, N), where A is an anchor, P is a positive example which is similar to A, and N is a negative example which is dissimilar to A. For example <a href="https://repositori.upf.edu/bitstream/handle/10230/54158/Alonso_ismir_musi.pdf">if we wanted to learn embeddings for songs</a>, we might say a positive example is a spectrogram clip from the same artist as the anchor, and a negative sample is from a different artist.</p>
<p style="text-align: center; font-size: 1.5em">
$$
\text{L} = \sum_{i=1}^N \text{max} \left(0, cos( f(a_i) - f(p_i) ) - cos( f(a_i) - f( n_i )) + m \right)
$$
</p>
<p>for cosine similarity or for euclidean distiance,</p>
<p style="text-align: center; font-size: 1.5em">
$$
\text{L} = \sum_{i=1}^N \text{max} \left(0, \lVert f(a_i) - f(p_i) \rVert_2 - \lVert f(a_i) - f( n_i ) \rVert_2 + m \right)
$$
</p>
<p>In other words, we enforce that the distance between the anchor and the negative sample is greater than the distance between the anchor and the positive sample <em>plus</em> some margin. For cosine similarity, what this would look like is that your vectors appear as points on the perimeter of a high-dimensional clock face (i.e. the surface of an N-dimensional hypersphere). Positive samples grow closer to the anchor while the negative samples are pushed away. The higher the margin, the closer positive samples will be pushed together.</p>
<figure><img src="/img/contrastive_learning.png"
			alt="Learning to differentiate Bangarang and Clair de lune. Triplet loss pushes positive examples closer to the anchor and negatives farther away. Some say Claude DeBussy was the Skrillex of 1890."><figcaption>
			<p>Learning to differentiate Bangarang and Clair de lune. Triplet loss pushes positive examples closer to the anchor and negatives farther away. Some say Claude DeBussy was the Skrillex of 1890.</p>
		</figcaption>
</figure>

<h2 id="contrastive-learning-contrastive-loss">Contrastive learning: Contrastive loss</h2>
<p>If you have binary labels, you can use <a href="http://yann.lecun.com/exdb/publis/pdf/hadsell-chopra-lecun-06.pdf">contrastive loss</a>. It works with positive pairs <code>(anchor, positive)</code> and negative pairs <code>(anchor, negative)</code>, denoted with a binary label. In the original paper, $ y \in [1,0] $ is the label indicating the desired distance between the points (<strong>not</strong> whether they are similar).</p>
<p>Like triplet loss, contrastive loss also includes a margin parameter. For dissimilar pairs, a loss is incurred only if their predicted distance falls within the margin&rsquo;s radius.</p>
<p style="text-align: center; font-size: 1.5em">
$$
\text{loss} = \frac{y_{i}(D_{W_i})^2 + (1 - y_i) \left( \text{max}(0, m - D_{W_i}) \right) ^ 2 }{2}
$$
</p>
<p>Where $ D_{W_i} $ is euclidean distance between the two embeddings of sentences $ a_i $ and $ b_i $:</p>
<p style="text-align: center; font-size: 1.5em">
$$
D_{W_i} = \lVert f(a_i) - f(b_i) \rVert _{2}
$$
</p>
<p>Note that triplet loss is often confused with contrastive loss, but they are not the same.</p>
<h2 id="regression-cosine-similarity-loss">Regression: Cosine similarity loss</h2>
<p>If you already have some numerical labels for the similarity of two pieces of text, you can compute mean squared error between their predicted cosine similarity and the actual similarity.</p>
<p>For pairs of sentences a<sub>i</sub> and b<sub>i</sub> and label y<sub>i</sub>,</p>
<p style="text-align: center; font-size: 1.5em">
$$
\text{loss} = \frac{1}{N} \sum_{i=1}^{N} \left[ cos\left(f(a_{i}), f(b_{i})\right) - y_{i} \right] ^ 2
$$
</p>
<h2 id="regression-cosent-loss">Regression: CoSENT loss</h2>
<p>CoSENT loss is an improved version of cosine similarity learning which makes better use of in-batch values. For a batch with size <code>N</code> it makes up to <code>Nx(N-1)/2</code> predictions. (I.e. for a batch size of 10 we make up to 45 predictions rather than the 10 in Cosine Similarity Loss.) It is essentially a ranking loss, taking advantage of the knowledge of the relative similarities within the batch.</p>
<p style="text-align: center; font-size: 1.5em">
$$
\text{loss} = \log \left( 1 + \sum_{\text{sim}(i,j) > \text{sim}(k,l)} e^{\lambda (\cos(k, l) - \cos(i, j))} \right)
$$
</p>
<p>Experiments have shown <a href="https://github.com/UKPLab/sentence-transformers/pull/2454#issuecomment-1914664814">improved performance</a> over Cosine Similarity loss.</p>
<h2 id="using-them-sub-linear-vector-search">Using them: Sub-linear Vector Search</h2>
<p>Once we have generated our vectors, we will want to search through them as quickly as possible. If our application is maintaining a large (i.e. million or billions of items) catalogue, linear scan through them all will be too slow.</p>
<p>Instead, we can <a href="/posts/how-does-hnsw-work/">use an approximate nearest neighbors algorithm like HNSW</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Vectorized K-Means Clustering</title>
            <link>https://blog.lukesalamone.com/posts/vectorized-kmeans/</link>
            <pubDate>Sun, 04 Feb 2024 23:39:29 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/vectorized-kmeans/</guid>
            <description>&lt;p&gt;K-means clustering (&lt;a href=&#34;../posts/kmeans-clustering/&#34;&gt;previous discussion&lt;/a&gt;) is an unsupervised learning algorithm which assigns points to one of K different clusters based on the distance of that point to a centroid. The points may represent physical locations, or embeddings in high-dimensional vector space.&lt;/p&gt;
&lt;p&gt;🌟Check out the demo (in two dimensions) below. Centroids are colored white.🌟&lt;/p&gt;
&lt;script src=&#34;https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js&#34;&gt;&lt;/script&gt;
&lt;script src=&#34;../js/kmeans_demo.js&#34;&gt;&lt;/script&gt;
&lt;div id=&#39;demo&#39;&gt;
&lt;button style=&#34;border:1px solid #09f&#34;&gt;start&lt;/button&gt;
&lt;canvas id=&#34;myChart&#34; style=&#34;background-color: #0000&#34; width=&#34;500px&#34; height=&#34;500px&#34;&gt;&lt;/canvas&gt;
&lt;/div&gt;
&lt;p&gt;Note that the points are changing color only, not moving.&lt;/p&gt;
&lt;h2 id=&#34;general-algorithm&#34;&gt;General algorithm&lt;/h2&gt;
&lt;p&gt;The basic K-means algorithm is fairly simple and has two steps, repeated until convergence (i.e. when no points change cluster):&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>K-means clustering (<a href="/posts/kmeans-clustering/">previous discussion</a>) is an unsupervised learning algorithm which assigns points to one of K different clusters based on the distance of that point to a centroid. The points may represent physical locations, or embeddings in high-dimensional vector space.</p>
<p>🌟Check out the demo (in two dimensions) below. Centroids are colored white.🌟</p>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<script src="/js/kmeans_demo.js"></script>
<div id='demo'>
<button style="border:1px solid #09f">start</button>
<canvas id="myChart" style="background-color: #0000" width="500px" height="500px"></canvas>
</div>
<p>Note that the points are changing color only, not moving.</p>
<h2 id="general-algorithm">General algorithm</h2>
<p>The basic K-means algorithm is fairly simple and has two steps, repeated until convergence (i.e. when no points change cluster):</p>
<ol>
<li>assign points to the cluster corresponding to closest centroid</li>
<li>update the centroid locations to the mean of all points assigned to the associated cluster</li>
</ol>
<pre><code class="language-python">import numpy as np

'''
General setup: we will demo 5000 points, each with two 
dimensions. They will be clustered into 5 clusters.
'''

N, k, d = 5000, 5, 2
points = np.random.random((N,d))
centroids = np.random.random((k,d))

while True:

  # assign each point to closest cluster
  clusters = assign_clusters(points, centroids)

  # recalculate centroids
  new_centroids = update_centroids(points, clusters, k)

  # if centroids haven't changed, we're done
  if centroids_equal(new_centroids, centroids):
    break

  # otherwise, update centroids
  centroids = new_centroids
</code></pre>
<h2 id="vectorizing-the-algorithm">Vectorizing the algorithm</h2>
<p>Now that we&rsquo;ve established the general algorithm, we might consider how it can be vectorized. In essence, this means we&rsquo;ll avoid writing for-loops in python. In numpy, this means cython will handle for-loops. For environments with a GPU, we can use PyTorch, JAX or Tensorflow to take advantage of parallel processing that GPUs excel at.</p>
<h3 id="how-broadcasting-works">How broadcasting works</h3>
<p>Before going further it would be good to pause and introduce broadcasting. The <a href="https://numpy.org/devdocs/user/basics.broadcasting.html">numpy guide</a> is much more comprehensive, but the gist is that because numpy needs tensors to have the same shape before performing certain arithmetic operations on them, numpy will automatically try to align their shapes. Importantly, this is done without making copies of the data. The steps for broadcasting are as follows:</p>
<ol start="0">
<li>A scalar will simply take the shape of the vector it&rsquo;s being broadcasted to.</li>
<li>If one vector has fewer dimensions, left-pad its shape with 1s.</li>
<li>Starting from the right, check dimension compatibility. If they are equal or one of them is 1, the dimensions are compatible.</li>
</ol>
<p>For example,</p>
<pre><code class="language-python"># scalars are the simplest
(np.ones((2,2)) * 3).shape # (2,2)

# (3,) is left padded to (1,3)
(np.ones((3)) * np.ones((2,3))).shape # (2,3)

# if dimensions equal or one of them is 1, they are compatible
(np.ones((2,3,1)) * np.ones((2,1,4))).shape # (2,3,4)

# error! [1,1] * [1,1,1] doesn't work
(np.ones((2)) * np.ones(3)) # incompatible shapes!
</code></pre>
<p>For more information, check out the Numpy guide: <a href="https://numpy.org/devdocs/user/basics.broadcasting.html">https://numpy.org/devdocs/user/basics.broadcasting.html</a>.</p>
<h3 id="assign_clusters">assign_clusters()</h3>
<p>Ok, back to K-means. Let&rsquo;s start with <code>assign_clusters()</code>. A point is assigned to the cluster corresponding to the centroid which is closest.</p>
<p>The general idea is that for each point we&rsquo;d like to know its distance to each centroid. We can then take the argmin of those distances to identify the correct cluster and return a result with shape <code>(N,)</code> (N = # of points). Distance will be computed using euclidean distance i.e. subtract, square, and sum.</p>
<p>First we expand the dimensions of the points from <code>(N,d)</code> to <code>(N,1,d)</code>. We also expand the dimensions of the centroids to <code>(1,k,d)</code>. (This happens automatically via broadcasting.) When we take the difference, we are left with a vector with shape <code>(N,k,d)</code>. For each point in <code>N</code>, this yields a vector with <code>k</code> rows and <code>d</code> columns.</p>
<p>Now that we have the differences, we square them, and take the sum across the last dimension. This yields a vector with shape <code>(N,k)</code>.</p>
<p>For actual euclidean distance we would then take the square root of the result, but since we just care about the relative distances we can skip this step.</p>
<p>Finally, we just take the <code>argmin</code> of the last dimension, i.e. for each row, the index of the smallest distance. Now we have our <code>(N,)</code> result.</p>
<pre><code class="language-python">def assign_clusters(points, centroids):
  # (5000,2) -&gt; (5000,1,2)
  points = np.expand_dims(points, 1)

  # (5000,1,2) - (5,2)
  # with broadcasting becomes (5000,5,2)
  diff = points - centroids

  diff = diff ** 2

  # (5000,5,2) -&gt; (5000,5)
  diff = diff.sum(-1)

  # (5000,5) -&gt; (5000,)
  diff = diff.argmin(-1)

  return diff
</code></pre>
<h3 id="update_centroids">update_centroids()</h3>
<p>To update the centroids, we simply take the mean of the points within that cluster. To do this, we will create a vector with shape <code>(N,k,d)</code> again, and mask out  points not in the cluster.</p>
<p>We start by creating an <code>N x k</code> mask, where 1s correspond to the n-th point being in the k-th cluster. We will also temporarily add a dimension at the end to make this <code>N x k x 1</code>.</p>
<p>Adding a dimension to the points vector for proper broadcasting, we can then create the <code>N x k x d</code> vector we planned above. Summing this across the first dimension yields a <code>k x d</code> vector, representing the sums of all of the points within each of the <code>k</code> clusters.</p>
<p>Now we just need to divide by the number of points in each cluster, which we can get by summing the <code>N x k</code> mask we created earlier across the first dimension, yielding <code>k</code> counts.</p>
<p>At this point, we can divide the sum by the counts to get the means.</p>
<pre><code class="language-python">def update_centroids(points, clusters, k):
    N = clusters.shape[0]

    # create an N x k mask, where each row contains a 1
    # in the position corresponding to its cluster
    mask = np.zeros((N, k))
    mask[np.arange(N), clusters] = 1

    # add a dimension in the middle so that 
    # multiplication will broadcast
    # (5000,2) -&gt; (5000,1,2)
    points = np.expand_dims(points, 1)

    # (5000,5,1) * (5000,1,2) results in a (5000,5,2) vector
    # for each of the 5000 points, we have a 5x2 vector where only
    # one of the five rows will have non-zero values
    masked_points = np.expand_dims(mask, -1) * points

    # sum across points
    # (5000, 5, 2) -&gt; (5, 2)
    sums = masked_points.sum(0)

    # sum across points
    counts = mask.sum(0)

    # avoid divide by zero
    counts[counts == 0] = 1

    # divide sum by count to get means
    return sums / np.expand_dims(counts, -1)
</code></pre>
<h3 id="centroids_equal">centroids_equal()</h3>
<p>As a last implementation, we need to check whether the centroids have changed. For this we can just call <code>np.array_equal()</code> on the old and new centroids vectors.</p>
<pre><code class="language-python">def centroids_equal(vec1, vec2):
  return np.array_equal(vec1, vec2)
</code></pre>
<h2 id="comparing-vectorization-vs-loops">Comparing vectorization vs loops</h2>
<p>To compare the vectorized implementation with the non-vectorized implementation, I ran each 100 times and took the mean clock time to converge. Note that because the points are random, K-means may take different numbers of iterations to converge.</p>
<p>The vectorized implementation (green) runs in significantly less time, between 7 and 21 times faster.</p>
<p><canvas id="groupedBarChart" width="800" height="400"></canvas></p>
<script>
        const vecSpeeds = {
          100: 1/0.00029814304085448387,
          1000: 1/0.002743517281487584,
          10000: 1/0.038284409288316966,
          100000: 1/0.5512656086706557
        };

        const loopSpeeds = {
          100: 1/0.0023364973906427624,
          1000: 1/0.04799017548095435,
          10000: 1/0.720272407180164,
          100000: 1/10.535213513069321
        };

        let mult = [7.637348888, 14.978150694, 21.25242236];

        const labels = Object.keys(vecSpeeds);
        const vecData = Object.values(vecSpeeds);
        const loopData = Object.values(loopSpeeds);

        const data = {
            labels: labels,
            datasets: [{
                label: 'Vectorized',
                data: vecData,
                backgroundColor: '#00ff0022',
                borderColor: '#00ff00',
                borderWidth: 1
            }, {
                label: 'Non-vectorized',
                data: loopData,
                backgroundColor: '#8a2be222',
                borderColor: '#8a2be2',
                borderWidth: 1
            }]
        };

        const config = {
            type: 'bar',
            data: data,
            options: {
              scales: {
                  y: {
                      type: 'logarithmic',
                      beginAtZero: true,
                      title: {
                          display: true,
                          text: 'runs/sec (log scale)'
                      }
                  },
                  x: {
                    title: {
                      display: true,
                      text: 'number of points'
                    }
                  }
              },
              plugins: {
                title: {
                    display: true,
                    text: 'K-means speeds (k=5)',
                    font: {
                        size: 16
                    },
                    padding: {
                        top: 10,
                        bottom: 30
                    }
                }
              }
            }
        };

        const myChart = new Chart(
            document.getElementById('groupedBarChart'),
            config
        );
</script>]]></content>
        </item>
        
        <item>
            <title>Summary: Deep &amp; Cross Net v2</title>
            <link>https://blog.lukesalamone.com/posts/deep-cross-net-v2/</link>
            <pubDate>Mon, 02 Oct 2023 12:39:18 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/deep-cross-net-v2/</guid>
            <description>&lt;p&gt;Paper link: &lt;a href=&#34;https://arxiv.org/pdf/2008.13535&#34;&gt;https://arxiv.org/pdf/2008.13535&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Learning to rank is an important problem in many machine-learning products such as search, recommendation, and advertising. Originally, many machine learning systems used simple logistic regression models, but it quickly became apparent that combining two or more features together was &lt;a href=&#34;https://www.ismll.uni-hildesheim.de/pub/pdfs/Rendle2010FM.pdf&#34;&gt;even better&lt;/a&gt;. This is called feature crossing.&lt;/p&gt;
&lt;p&gt;A lot of research and engineering work has gone into learning useful feature crosses. The fundamental problem is that although higher-order feature crosses can be more informative, they are also more sparse, and the number of high order features grows exponentially. Some attempts to address this have been:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Paper link: <a href="https://arxiv.org/pdf/2008.13535">https://arxiv.org/pdf/2008.13535</a></p>
<p>Learning to rank is an important problem in many machine-learning products such as search, recommendation, and advertising. Originally, many machine learning systems used simple logistic regression models, but it quickly became apparent that combining two or more features together was <a href="https://www.ismll.uni-hildesheim.de/pub/pdfs/Rendle2010FM.pdf">even better</a>. This is called feature crossing.</p>
<p>A lot of research and engineering work has gone into learning useful feature crosses. The fundamental problem is that although higher-order feature crosses can be more informative, they are also more sparse, and the number of high order features grows exponentially. Some attempts to address this have been:</p>
<table>
	<thead>
			<tr>
					<th>Architecture</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><a href="https://arxiv.org/pdf/1606.07792">Wide &amp; Deep network</a></td>
					<td>Contains a wide linear model to capture feature interactions and a deep neural net (DNN) to capture nonlinear relationships.</td>
			</tr>
			<tr>
					<td><a href="https://arxiv.org/pdf/1703.04247">DeepFM</a></td>
					<td>Combines a FM component to capture second-order feature interactions and a DNN to capture higher-order interactions.</td>
			</tr>
			<tr>
					<td><a href="https://arxiv.org/pdf/1803.05170">xDeepFM</a></td>
					<td>Extends DeepFM by adding a compressed interaction network to model high-order vector-wise feature interactions.</td>
			</tr>
			<tr>
					<td><a href="https://arxiv.org/pdf/1906.00091">DLRM</a></td>
					<td>Uses a DNN for dense features, an embedding lookup for sparse features, dot product interaction layers, and a final DNN.</td>
			</tr>
			<tr>
					<td><a href="https://arxiv.org/pdf/1611.00144">PNN</a></td>
					<td>Uses a product layer to explicitly model pairwise feature interactions, followed by a neural net.</td>
			</tr>
			<tr>
					<td><a href="https://arxiv.org/pdf/1810.11921">AutoInt</a></td>
					<td>Uses multi-head self-attention to model feature interactions, followed by a DNN.</td>
			</tr>
			<tr>
					<td><a href="https://arxiv.org/pdf/1708.05123">DCN</a></td>
					<td>Explicitly applies feature crosses with a Cross network, followed by a DNN.</td>
			</tr>
	</tbody>
</table>
<h2 id="dcnv2">DCNv2</h2>
<p>DCNv2 improves on the original DCN architecture in a few important ways. First, it uses a more expressive feature cross formula. Second, it introduces matrix factorization to dramatically reduce the required number of parameters in the model. Finally, the authors also introduce a gated mixture of experts setup to take advantage of the parameter savings.</p>
<figure><img src="/img/dcn_archs.png"
			alt="DCNv2 can be implemented with a stacked (i.e. serial) architecture or a parallel architecture."><figcaption>
			<p>DCNv2 can be implemented with a stacked (i.e. serial) architecture or a parallel architecture.</p>
		</figcaption>
</figure>

<h3 id="embedding-layer">Embedding layer</h3>
<p>To create the embedding layer, all features are concatenated together to create a tensor with shape <code>batch size x embedding size</code>. For one-hot encoded categorical features, they can be projected to a much lower dimensionality with a learned matrix (i.e. a single dense layer with no bias term).</p>
<p>One convenient property of this embedding scheme is that feature order doesn&rsquo;t matter. All features will be crossed with all other features, so we don&rsquo;t need to worry about the way that features are added and removed.</p>
<h3 id="cross-net">Cross Net</h3>
<p>The formula for a single cross operation is</p>
<pre><code class="language-python">&quot;&quot;&quot;
Dimension key:
    B: batch size
    D: embedding dimension

Params:
    x_0: original embedding, shape BD
    x_i: i-th embedding, shape BD
    weight_i: i-th learned weight matrix, shape DD
    bias_i: i-th learned bias term, shape D

Returns:
    The residual embedding after i+1 cross operations
&quot;&quot;&quot;

def cross(x_0, x_i, weight_i, bias_i):
    x_0 = x_0.unsqueeze(-1)         # (B, D, 1)
    x_i = x_i.unsqueeze(-1)         # (B, D, 1)
    bias_i = bias_i.unsqueeze(-1)   # (D, 1)

    kernel = weight_i @ x_i     # (D, D) @ (B, D, 1) = (B, D, 1)
    kernel = kernel + bias_i    # (B, D, 1) + (D, 1) = (B, D, 1)
    
    result = x_0 * kernel + x_i # (B, D, 1)
    return result.squeeze(-1)   # (B, D)
</code></pre>
<p>Note that the <code>weight_i @ x_i + bias_i</code> term is equivalent to a fully-connected neural network layer with no activation. To this result, we element-wise multiply the original embedding, then add the i-th embedding.</p>
<h4 id="low-rank-approximation">Low rank approximation</h4>
<p>Of course, with high embedding dimensionality, the weight matrix can easily grow to millions of parameters, and we need a weight matrix for each of the cross operations. A common technique to reduce the number of parameters is to use a low rank approximation of the weight matrix. In other words, we can replace the <code>weight @ x</code> operation with <code>weight_u @ (weight_v @ x)</code></p>
<p>In particular, rather than requiring <code>DxD</code> parameters for each cross operation, we will now only need <code>DxR + RxD = 2xRxD</code> parameters, where <code>R</code> is the low-rank approximation dimension. So if <code>R</code> is less than half of <code>D</code>, we can save parameters using this technique.</p>
<pre><code class="language-python">&quot;&quot;&quot;
Dimension key:
    B: batch size
    D: embedding dimension
    R: low rank dimension

Params:
    x0: original embedding, shape BD
    xi: i-th embedding, shape BD
    weight_u_i: i-th learned weight matrix, shape DR
    weight_v_i: i-th learned weight matrix, shape RD
    bias_i: i-th learned bias term, shape D

Returns:
    The residual embedding after i+1 cross operations
&quot;&quot;&quot;

def low_rank_cross(x_0, x_i, weight_u_i, weight_v_i, bias_i):
    x_0 = x_0.unsqueeze(-1)         # (B, D, 1)
    x_i = x_i.unsqueeze(-1)         # (B, D, 1)
    bias_i = bias_i.unsqueeze(-1)   # (D, 1)

    kernel = weight_v_i @ x_i       # (R, D) @ (B, D, 1) = (B, R, 1)
    kernel = weight_u_i @ kernel    # (D, R) @ (B, R, D) = (B, D, 1)
    kernel = kernel + bias_i        # (B, D, 1) + (D, 1) = (B, D, 1)
    
    result = x_0 * kernel + x_i     # (B, D, 1)
    return result.squeeze(-1)       # (B, D)
</code></pre>
<h2 id="mixture-of-low-rank-experts">Mixture of low-rank experts</h2>
<p>The parameter savings from using low-rank approximations of the weight matrices can be reinvested into additional parameters. In particular, if we have a gating function <code>G(x)</code> we can use it to decide between a group of &ldquo;experts&rdquo; which are themselves low-rank Cross networks.</p>
<figure><img src="/img/mixture_of_experts_dcn.png"
			alt="A mixture of experts consists of a gating function and an expert function. The gating function determines the weight of the expert function."><figcaption>
			<p>A mixture of experts consists of a gating function and an expert function. The gating function determines the weight of the expert function.</p>
		</figcaption>
</figure>

<p>Note that setting <code>G(x)=1</code> and <code>K=1</code> reduces the mixture of experts to the original low-rank DCN architecture.</p>
<h2 id="analysis-of-hyperparameters">Analysis of hyperparameters</h2>
<p>The number of cross layers (i.e. cross depth), the choice of low-rank approximation, and the number of experts can each have a large effect on the model performance. For the depth of the crosses, the researchers found that performance improved with the number of cross layers, but diminishing returns as depth increased.</p>
<p>The behavior of the matrix rank was interesting. For very low rank, the model performed similarly with other baselines. However, increasing from rank 4 to 64 showed dramatic performance improvement, and notable but lesser improvement afterwards. This suggests there is some &ldquo;elbow&rdquo; (the authors call this a <em>threshold rank</em>) which maintains good performance without consuming too many parameters.</p>
<p>For the number of experts, the researchers found that the <em>total rank</em> sum of all of the experts was the main contributing factor for performance. In other words, 1x256, 4x64, 8x32, 16x16 and 32x8 all performed similarly. They hypothesized that their simple gating function was responsible for this.</p>
<h2 id="results-on-synthetic-datasets">Results on synthetic datasets</h2>
<p>To better understand how the DCN, DCNv2 and DNN are able to model cross interactions, the authors created synthetic functions for the various architectures to learn. They created three classes of functions with increasing difficulty to learn.</p>
<figure><img src="/img/synthetic_functions_dcn.png"
			alt="Synthetic functions of increasing difficulty to be learned by DCN, DCNv2 and DNN."><figcaption>
			<p>Synthetic functions of increasing difficulty to be learned by DCN, DCNv2 and DNN.</p>
		</figcaption>
</figure>

<p>Note that the third equation can be modeled by a single cross operation.</p>
<p>In their experiments, the authors found:</p>
<ul>
<li>The DCN consistently used the fewest parameters (maximum of 300 parameters) and was able to accurately model the simpler functions, but was unable to accurately model the third, most complicated function with a single layer. However, since a single layer was only 300 parameters, experimenting with a larger model would have been interesting.</li>
<li>The DNN used the most parameters (up to 758k parameters) and was unable to model any of the functions as accurately as either DCN or DCNv2.</li>
<li>The DCNv2 used up to 10k parameters for the most complex function and achieved the lowest RMSE loss by far.</li>
</ul>
<p>The authors also directly compared DCNv2 models with varying numbers of layers with a traditional DNN in modeling a more complex function containing sine terms and polynomials of order 1-4 and found that DNNs are highly inefficient in modeling higher order interactions.</p>
<h2 id="results-on-criteo">Results on Criteo</h2>
<p>Criteo is a popular click-through-rate (CTR) prediction dataset containing 7 days of user logs. It contains 13 continuous features and 26 categorical features.</p>
<p>On Criteo, DCN-v2 (best setting using two layers) had a 0.21% better AUC than a DNN baseline. DCN-Mix (best setting using 3 layers, 4 experts, and low rank of 258) was 0.17% better on AUC than the DNN baseline.</p>
<h2 id="results-on-movielens-1m">Results on MovieLens-1M</h2>
<p>MovieLens-1M is a popular dataset for recommendation systems research, containing one million samples of [user-features, movie-features, rating] triplets. The task was treated as a regression problem. Ratings of 1 and 2 were set to 0 and 4 and 5 were set to 1. 3s were removed.</p>
<p>On MovieLens, DCN-V2 was 0.24% better on AUC than the DNN baseline. DCN-Mix was even better, at +0.39% over the DNN baseline.</p>
]]></content>
        </item>
        
        <item>
            <title>What is a blunder in chess?</title>
            <link>https://blog.lukesalamone.com/posts/chess-blunders/</link>
            <pubDate>Mon, 25 Sep 2023 20:47:30 -0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/chess-blunders/</guid>
            <description>&lt;p&gt;What is a blunder in chess? The tension between the qualitative and quantitative answers to this question is at the heart of different approaches towards chess, and more broadly, how quantitative metrics may lack context, but qualitative metrics lack precision.&lt;/p&gt;
&lt;h2 id=&#34;qualitative-answer&#34;&gt;Qualitative answer&lt;/h2&gt;
&lt;p&gt;There are many qualitative answers to this question, especially when comparing &amp;ldquo;blunders&amp;rdquo; and &amp;ldquo;mistakes&amp;rdquo;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;a move that negatively affects their position in a significant way&amp;rdquo; ~ &lt;a href=&#34;https://www.chess.com/terms/chess-blunder&#34;&gt;chess.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;severely worsens the player&amp;rsquo;s situation by allowing a loss of material, checkmate, or anything similar&amp;rdquo; ~ &lt;a href=&#34;https://en.wikipedia.org/wiki/Blunder_%28chess%29&#34;&gt;Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Blunders tend to be immediately refutable, while mistakes require planning to capitalize on.&amp;rdquo; ~ &lt;a href=&#34;https://www.reddit.com/r/chess/comments/1iiqyb/what_distinguishes_the_difference_between_a/&#34;&gt;r/chess&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More generally, &lt;a href=&#34;../posts/qualitative-analysis-chess/&#34;&gt;qualitative analysis is happy to consider more abstract aspects of a position&lt;/a&gt; than piece count, such as the positional strength of an active knight, king safety, and &amp;ldquo;practical chances&amp;rdquo; i.e. how easy it is for a human to play the position.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>What is a blunder in chess? The tension between the qualitative and quantitative answers to this question is at the heart of different approaches towards chess, and more broadly, how quantitative metrics may lack context, but qualitative metrics lack precision.</p>
<h2 id="qualitative-answer">Qualitative answer</h2>
<p>There are many qualitative answers to this question, especially when comparing &ldquo;blunders&rdquo; and &ldquo;mistakes&rdquo;:</p>
<ul>
<li>&ldquo;a move that negatively affects their position in a significant way&rdquo; ~ <a href="https://www.chess.com/terms/chess-blunder">chess.com</a></li>
<li>&ldquo;severely worsens the player&rsquo;s situation by allowing a loss of material, checkmate, or anything similar&rdquo; ~ <a href="https://en.wikipedia.org/wiki/Blunder_%28chess%29">Wikipedia</a></li>
<li>&ldquo;Blunders tend to be immediately refutable, while mistakes require planning to capitalize on.&rdquo; ~ <a href="https://www.reddit.com/r/chess/comments/1iiqyb/what_distinguishes_the_difference_between_a/">r/chess</a></li>
</ul>
<p>More generally, <a href="/posts/qualitative-analysis-chess/">qualitative analysis is happy to consider more abstract aspects of a position</a> than piece count, such as the positional strength of an active knight, king safety, and &ldquo;practical chances&rdquo; i.e. how easy it is for a human to play the position.</p>
<p>An issue with these qualitative answers is that while their words may be correct, smart people may still disagree with their applicability at the margins. For a suboptimal move to have a &ldquo;significant&rdquo; negative effect, it requires that the opponent notices and takes advantage of it.</p>
<h2 id="quantitative-answer">Quantitative answer</h2>
<p>The quantitative answer considers a move which causes a significant drop in probability of winning to be blunder. What is &ldquo;significant&rdquo;? <a href="https://github.com/lichess-org/lila/blob/a02b6fb2b76edfd4c73c36b2e022d2c9265dbb42/ui/analyse/src/practice/practiceCtrl.ts#L116">A change 14% or greater</a>.</p>
<p>How is winning probability in a chess game calculated? Objectively, since there are only three possible outcomes in a game (win, draw, loss), by definition any real advantage will lead to a win with perfect play. But objectively, humans aren&rsquo;t perfect. Even grandmasters can let an advantage slip. If <a href="https://www.youtube.com/watch?v=d7Vc0fXoTNU">Magnus Carlsen doesn&rsquo;t capitalize on your blunder</a>, was it really a blunder?</p>
<p>From a machine learning perspective, we can view winning probability as a logistic regression problem, where centipawn evaluation is a feature, and game outcome is a label. If we further limit data points to 2300+ rated games, <a href="https://github.com/lichess-org/lila/pull/11148">this is what Lichess uses</a>.</p>
<div style="width:100%; display: flex; justify-content: center;">
  <iframe src="https://www.desmos.com/calculator/ieqrq3ry9z?embed" width="500" height="500" frameborder=0></iframe>
</div>
<p>However, <a href="https://lichess.org/page/accuracy">Lichess&rsquo; accuracy article</a> suggests that this relationship is slightly flawed in two ways:</p>
<ol>
<li>Higher-rated players tend to play more theoretical openings which will have a lower centipawn loss.</li>
<li>Higher-rated players tend to resign in lost positions, perhaps underestimating their true win percentage due to opponent blunders. <a href="https://www.youtube.com/watch?v=5zfWLcwUsIM">Never resign!</a></li>
</ol>
<p>Of course, this isn&rsquo;t perfect, and there&rsquo;s an argument to be made that outcomes for 2300+ elo players may not be representative of lower-rated players. It also doesn&rsquo;t take time pressure into consideration. Taken to the extreme, we can even use an <a href="/posts/winning-faster-than-stockfish/">adversarial player-modeling algorithm</a> to win faster than Stockfish. But there is a tradeoff between accuracy of our metric and generalization power of the model.</p>
<p>(We can also go the opposite direction and directly <a href="/posts/distilling-stockfish/">try to quickly predict the stockfish evaluation of a position</a>.)</p>
<p><a href="https://zwischenzug.substack.com/p/centipawns-suck">One other important objection</a> to this line of inquery is that the centipawn evaluation of a position is not a constant. The evaluation varies by search depth and between engines. So if Stockfish 15 evaluates a position at +1 and Stockfish 16 evaluates the same position at +1.5, white&rsquo;s actual winning chances haven&rsquo;t changed at all. The evaluation is not anchored to any real value, especially with the introduction of NNUE eval functions.</p>
]]></content>
        </item>
        
        <item>
            <title>A 3D Game of Life</title>
            <link>https://blog.lukesalamone.com/posts/game-of-life-3d/</link>
            <pubDate>Wed, 23 Aug 2023 23:34:38 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/game-of-life-3d/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life&#34;&gt;Conway&amp;rsquo;s Game of Life&lt;/a&gt; is a simulation developed in 1970 describing a grid of binary cells and transition rules for each cell which depend on the state of the cell&amp;rsquo;s neighbors. It&amp;rsquo;s capable of creating some pretty cool patterns.&lt;/p&gt;
&lt;p&gt;This variant of the Game of Life uses three overlapping channels, so instead of just one simulation, there are three simultaneous simulations. I visualize these in the three color channels, red, green and blue. Two or more channels active on the same cell are represented with &lt;a href=&#34;https://en.wikipedia.org/wiki/Additive_color&#34;&gt;additive color mixing&lt;/a&gt;.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><a href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life">Conway&rsquo;s Game of Life</a> is a simulation developed in 1970 describing a grid of binary cells and transition rules for each cell which depend on the state of the cell&rsquo;s neighbors. It&rsquo;s capable of creating some pretty cool patterns.</p>
<p>This variant of the Game of Life uses three overlapping channels, so instead of just one simulation, there are three simultaneous simulations. I visualize these in the three color channels, red, green and blue. Two or more channels active on the same cell are represented with <a href="https://en.wikipedia.org/wiki/Additive_color">additive color mixing</a>.</p>
<p>These additional channels enable additional interaction between channels, which is pretty neat. I&rsquo;ve added a rule which revives a dead cell if the sum of its active neighbors from other channels is 9. One nice result of this inter-channel interaction is that stagnant colors can be reactivated by other colors that pass by.</p>
<p>Finding parameters that don&rsquo;t cause the simulation to blow up or die out is a bit of a trial and error. I made an empirical choice which results in a lively &ldquo;pond&rdquo; after the chaotic initial state. I&rsquo;m sure there are other nice combinations as well.</p>
<p>Other variants I experimented with briefly were to include more than three channels and to consider more complex relationships between the channels. However these ran slower and/or were harder to balance between chaotic and desolate tendencies.</p>
<div id="game-of-life-controls">
  <button class="start">start game</button>
  <button class="pause" style="display:none">pause game</button>
</div>
<div id="game-of-life-container"></div>
<script src="/js/game_of_life.js"></script>
<link rel="stylesheet" href="/css/game-of-life.css" />
]]></content>
        </item>
        
        <item>
            <title>Can ChatGPT Recognize Handwritten Digits?</title>
            <link>https://blog.lukesalamone.com/posts/chatgpt-mnist/</link>
            <pubDate>Sun, 30 Jul 2023 22:45:57 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/chatgpt-mnist/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TLDR: No. No it cannot.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This was admittedly a fairly stupid experiment on the face of it. ChatGPT is a decoder-only model. It shouldn&amp;rsquo;t be able to perform an image recognition task. But then again, a decoder-only model wouldn&amp;rsquo;t have been my first choice for translation or summarization either. In my experience, ChatGPT has created translations which are at least as coherent and idiomatic as Google Translate, if not more so.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><strong>TLDR: No. No it cannot.</strong></p>
<p>This was admittedly a fairly stupid experiment on the face of it. ChatGPT is a decoder-only model. It shouldn&rsquo;t be able to perform an image recognition task. But then again, a decoder-only model wouldn&rsquo;t have been my first choice for translation or summarization either. In my experience, ChatGPT has created translations which are at least as coherent and idiomatic as Google Translate, if not more so.</p>
<p>So I thought, why not give it a shot?</p>
<h1 id="process">Process</h1>
<p><a href="https://pytorch.org/vision/main/generated/torchvision.datasets.MNIST.html#torchvision.datasets.MNIST">MNIST</a> is a computer vision dataset containing thousands of handwritten digits along with their actual labels. Images are represented as 28x28 tensors where each element of the tensor represents a intensity pixel intensity between 0 and 1. Traditionally this is formulated into a classification problem, with the goal of choosing the correct class out of the 10.</p>
<p>To simplify the input, I flattened the tensor and translated the pixel intensity values to the range 0-10. This allowed me to use fewer tokens at the expense of some of the gradations between light and dark.</p>
<p>I gathered 10 random examples from each of the 10 classes (100 total images) and fed them into <code>gpt-3.5-turbo</code> with the following prompt:</p>
<blockquote>
<p>The following is a flattened representation of an image of a handwritten digit. The image was 28x28 but has been flattened to 1x784. Each number represents a pixel intensity from 0-10. Please tell me which digit the following pixel list represents:</p>
</blockquote>
<p>Followed by the list of 784 pixel intensities.</p>
<h1 id="results">Results</h1>
<p>ChatGPT scored 11/100. Barely better than guessing. A summary of the sessions can be found at <a href="https://gist.github.com/lukesalamone/744272ef00e56afb6cab56cdc70a593a">this gist</a>. A visual summary is below, with the blue signifying correct guesses.</p>
<p><em>Note: although the image shows the digits sorted, they were prompted in random order.</em></p>
<p><img src="/img/chatgpt_mnist.png" alt="chatgpt performance on 100 handwritten digits"></p>
<p>It is interesting that only the 0s and the 7s were correctly identified. Let&rsquo;s take a look at the distribution of ChatGPT&rsquo;s guesses.</p>
<table>
	<thead>
			<tr>
					<th>Guess</th>
					<th>Count</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>0</td>
					<td>85</td>
			</tr>
			<tr>
					<td>1</td>
					<td>0</td>
			</tr>
			<tr>
					<td>2</td>
					<td>3</td>
			</tr>
			<tr>
					<td>3</td>
					<td>1</td>
			</tr>
			<tr>
					<td>4</td>
					<td>1</td>
			</tr>
			<tr>
					<td>5</td>
					<td>4</td>
			</tr>
			<tr>
					<td>6</td>
					<td>1</td>
			</tr>
			<tr>
					<td>7</td>
					<td>5</td>
			</tr>
			<tr>
					<td>8</td>
					<td>0</td>
			</tr>
			<tr>
					<td>9</td>
					<td>0</td>
			</tr>
	</tbody>
</table>
<p>Oh. It got 8/10 zeros correct because it guessed zero 85% of the time.</p>
]]></content>
        </item>
        
        <item>
            <title>Paper Summary: Antenna Design with Evolutionary Algorithms</title>
            <link>https://blog.lukesalamone.com/posts/evolutionary-antenna-design/</link>
            <pubDate>Mon, 17 Apr 2023 19:46:25 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/evolutionary-antenna-design/</guid>
            <description>&lt;p&gt;&lt;strong&gt;This is a summary of &lt;a href=&#34;https://www.researchgate.net/profile/Al-Globus/publication/228909002_Automated_Antenna_Design_with_Evolutionary_Algorithms/links/547375300cf216f8cfaff65a/Automated-Antenna-Design-with-Evolutionary-Algorithms.pdf&#34;&gt;Automated Antenna Design with Evolutionary Algorithms&lt;/a&gt;, a 2006 paper by Hornby et al. As large language models become more and more synonymous with &amp;ldquo;AI&amp;rdquo;, it is interesting to see how researchers solved problems in the past.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Typically, antennas are designed and built by hand by domain experts. This is a very time-consuming process, however, so researchers have been investigating evolutionary algorithms since the 1990s. Inspired by natural evolution, an evolutionary algorithm is based on small, random changes and an evaluation metric. In this paper, the authors describe the use of an evolutionary algorithm to design an antenna for a small satellite weighing only 25 kilograms called ST5.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><strong>This is a summary of <a href="https://www.researchgate.net/profile/Al-Globus/publication/228909002_Automated_Antenna_Design_with_Evolutionary_Algorithms/links/547375300cf216f8cfaff65a/Automated-Antenna-Design-with-Evolutionary-Algorithms.pdf">Automated Antenna Design with Evolutionary Algorithms</a>, a 2006 paper by Hornby et al. As large language models become more and more synonymous with &ldquo;AI&rdquo;, it is interesting to see how researchers solved problems in the past.</strong></p>
<p>Typically, antennas are designed and built by hand by domain experts. This is a very time-consuming process, however, so researchers have been investigating evolutionary algorithms since the 1990s. Inspired by natural evolution, an evolutionary algorithm is based on small, random changes and an evaluation metric. In this paper, the authors describe the use of an evolutionary algorithm to design an antenna for a small satellite weighing only 25 kilograms called ST5.</p>
<p>The researchers then describe the technical specifications which the antenna would need to satisfy. The antenna needed to weigh under 165 grams and have a height and diameter of around 15 centimeters.</p>
<p>In representing the antenna, the researchers used a tree structure, with each node capable of representing one of several operations: forward, rotate-x, rotate-y, and rotate-z. The “forward” operation adds a length of wire with a given radius. The “rotate-x” operation rotates the current state about the x-axis. The “rotate-y” and “rotate-z” operations do the same for the y and z-axes.</p>
<p>The researchers used a fitness function which was the product of VSWR, a gain_error term and a  gain_smoothness term. The gain_error term is similar to the least squares error function. The gain_smoothness term describes how uniform the gain pattern was, since the satellite would be spinning. All three of these factors were multiplied together to calculate an overall fitness score against which all candidate antennas were measured.</p>
<p>Due to a change in the orbit of the satellite, the specifications for the antenna were updated and the researchers needed to modify their evolutionary algorithm. This was completed within one month. The evolved antenna consumed less power, took less time to build, was less complex, and performed better than traditionally designed antennas. Since the satellite had 2 antennas, the researchers measured the combined performance. The evolved antennas were 93% efficient while the designed antennas were only 38% efficient. Additionally, the evolved antennas were much faster to design and fabricate.</p>
<p>The researchers then designed another antenna for NASA’s TDRS-C satellite. This design used an evolutionary algorithm in combination with a stochastic hill-climbing algorithm. They used a loss function which attended to the standing wave ratio and gain at several frequencies relevant to the satellite. In performing the evolutionary algorithm, 150 algorithm processes were run for 50,000 iterations after randomizing many parameters. After this first stage, the best antenna from the previous 150 was optimized using stochastic hill climbing with random mutations. From this second stage, the 23 best antennas were selected and run through another stochastic hill climbing step for 100,000 iterations. Of the 23 finalists, one of the evolved antennas exceeded the project specifications and was optimized further using more accurate software.</p>
]]></content>
        </item>
        
        <item>
            <title>Paper Summary: Dual-Encoders in Ranking</title>
            <link>https://blog.lukesalamone.com/posts/dual-encoders-ranking/</link>
            <pubDate>Sat, 17 Dec 2022 16:53:47 -0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/dual-encoders-ranking/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://proceedings.mlr.press/v162/menon22a/menon22a.pdf&#34;&gt;In Defense of Dual-Encoders for Neural Ranking by Menon et. al.&lt;/a&gt; discusses the question of why dual-encoder (DE) models, also called Bi-Encoders elsewhere, don&amp;rsquo;t match the performance of cross-attention (CA) models. The authors investigate what is actually going on, and demonstrate some improved performance over baseline DE models with a new model distillation method.&lt;/p&gt;
&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;Search requires an automatic way to find the most relevant documents to a query. There are bag-of-word approaches to this task (for example BM25) and neural approaches. An example of a bag-of-words approach might simply be to count the number of similar words between the query and each document, and return the document with the highest number of similar words. There are word-stuffing issues with this idea, but the larger issue is that a bag-of-words strategy can&amp;rsquo;t account for synonyms. If I search for &lt;em&gt;bad guy&lt;/em&gt; I will never find &lt;em&gt;villain&lt;/em&gt; without some additional logic to account for this. A neural network implicitly understands the relationship between words, and avoids the fragile logic of simple word counts.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><a href="https://proceedings.mlr.press/v162/menon22a/menon22a.pdf">In Defense of Dual-Encoders for Neural Ranking by Menon et. al.</a> discusses the question of why dual-encoder (DE) models, also called Bi-Encoders elsewhere, don&rsquo;t match the performance of cross-attention (CA) models. The authors investigate what is actually going on, and demonstrate some improved performance over baseline DE models with a new model distillation method.</p>
<h2 id="background">Background</h2>
<p>Search requires an automatic way to find the most relevant documents to a query. There are bag-of-word approaches to this task (for example BM25) and neural approaches. An example of a bag-of-words approach might simply be to count the number of similar words between the query and each document, and return the document with the highest number of similar words. There are word-stuffing issues with this idea, but the larger issue is that a bag-of-words strategy can&rsquo;t account for synonyms. If I search for <em>bad guy</em> I will never find <em>villain</em> without some additional logic to account for this. A neural network implicitly understands the relationship between words, and avoids the fragile logic of simple word counts.</p>
<p>The idea of a neural encoding approach is pretty simple. For each document in your corpus, pass the query a document into a function which will return a similarity score between 0 and 1. Then, just sort the documents by that score. There are two main architectures for doing this: dual-encoders and cross-attention models.</p>
<figure><img src="/img/dual_vs_cross_encoder.png"
			alt="Dual encoder architectures can precompute document embeddings but tend to be less accurate than cross attention models."><figcaption>
			<p>Dual encoder architectures can precompute document embeddings but tend to be less accurate than cross attention models.</p>
		</figcaption>
</figure>

<p>The great thing about DE models is that <a href="/posts/creating-tiny-semantic-search/">document embeddings can be computed ahead of time</a>. When users enter a query, only that query embedding needs to be calculated, and then compared with the embeddings already calculated for each of the documents. It&rsquo;s a lot faster. So much faster, in fact, that CA is generally not used for initial retrieval, only for reranking afterwards.</p>
<p>However, DE models tend to be less accurate than CA models. It would be great if it was possible to transfer some of the benefits of CA models to DE models.</p>
<h2 id="the-problem">The Problem</h2>
<p>It&rsquo;s unclear whether the shortcomings of DEs are due to the DE model&rsquo;s capacity or because of its training procedure. DEs may be overfitting.</p>
<figure><img src="/img/dual_encoder_overfitting.png"
			alt="DE models can match CA model training performance but are often lower during evaluation."><figcaption>
			<p>DE models can match CA model training performance but are often lower during evaluation.</p>
		</figcaption>
</figure>

<p>Dual encoders can also be improved by distillation, of which there are two kinds:</p>
<ol>
<li>Logit matching. Try to match embeddings between teacher and student.</li>
<li>Probability matching. Try to match the softmax probabilities between teacher and student.</li>
</ol>
<figure><img src="/img/de_ca_margins.png"
			alt="CA models have better separation between positive and negative examples, strongly predicting negative examples. DE models have more overlap between positive and negative predictions. Normalizing the margins between positive and negative predictions (higher is better), CA models clearly have better performance. The distilled model is slightly better than the DE model."><figcaption>
			<p>CA models have better separation between positive and negative examples, strongly predicting negative examples. DE models have more overlap between positive and negative predictions. Normalizing the margins between positive and negative predictions (higher is better), CA models clearly have better performance. The distilled model is slightly better than the DE model.</p>
		</figcaption>
</figure>

<p>Part of the cause of this discrepancy may be the fact that DE models have noisier updates. DE models may have difficulty modeling negative scores since updating their weights on positive (q, d+) pairs can inadvertently increase scores for negative (q, d-) pairs. Dropout also doesn&rsquo;t seem to mitigate overfitting.</p>
<figure><img src="/img/de_ca_score_evolution.png"
			alt="The evolution of scores for five positive and five negative documents for a fixed query. Scores from the CA model separate much more smoothly than in the DE model."><figcaption>
			<p>The evolution of scores for five positive and five negative documents for a fixed query. Scores from the CA model separate much more smoothly than in the DE model.</p>
		</figcaption>
</figure>

<h2 id="solutions">Solutions</h2>
<p>Previous work has tried to improve training procedures in several ways:</p>
<ol>
<li>Adjusting the scoring layer. Usually embeddings are scored with a simple dot product, but a more sophisticated scoring function may be able to capture more information at the cost of inference speed.</li>
<li>Distilling predictions from CA models. Model distillation uses a teacher-student framework where the smaller &ldquo;student&rdquo; model attempts to mirror the &ldquo;teacher&rdquo;. This paper explores a new approach to distillation.</li>
</ol>
<p>The authors introduce <em>multi-margin MSE loss</em> (M3SE):</p>
<figure><img src="/img/m3se.png">
</figure>

<p>M3SE loss attempts to match the margins of score differences between teacher and student. For performance reasons, however, rather than matching each margin, it only encourages the student to be less than or equal to the teacher&rsquo;s highest negative score.</p>
<p>M3SE can be seen as an extension of Margin MSE loss where instead of matching logits it matches raw scores. It can also be seen as a smooth approximation of softmax cross-entropy loss. The authors also highlight parallels between M3SE and RankDistil.</p>
<h2 id="results">Results</h2>
<figure><img src="/img/m3se_results.png"
			alt="Apart from TREC, M3SE distillation appears to nearly close the gap with cross-attention models. Distilled models are 6-layer BERT models with embedding size 768."><figcaption>
			<p>Apart from TREC, M3SE distillation appears to nearly close the gap with cross-attention models. Distilled models are 6-layer BERT models with embedding size 768.</p>
		</figcaption>
</figure>

]]></content>
        </item>
        
        <item>
            <title>My Favorite Antimaia Games</title>
            <link>https://blog.lukesalamone.com/posts/best-antimaia-games/</link>
            <pubDate>Sat, 26 Nov 2022 20:25:13 -0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/best-antimaia-games/</guid>
            <description>&lt;p&gt;This is a follow up to &lt;a href=&#34;../posts/winning-faster-than-stockfish/&#34;&gt;Opponent Modeling Wins 2× Faster Than Stockfish&lt;/a&gt;. After running 400 simulations, I can conclusively say that opponent modeling is pretty cool.&lt;/p&gt;
&lt;p&gt;The TLDR on opponent modeling is that if we have a pretty good idea of what the opponent might do, we can beat them faster by playing moves which aren&amp;rsquo;t objectively &amp;ldquo;optimal&amp;rdquo; as far as minimax is concerned. Here, Maia 1900 is a model of a relatively high-level chess player. Antimaia 1900 is specifically designed to counter Maia 1900.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>This is a follow up to <a href="/posts/winning-faster-than-stockfish/">Opponent Modeling Wins 2× Faster Than Stockfish</a>. After running 400 simulations, I can conclusively say that opponent modeling is pretty cool.</p>
<p>The TLDR on opponent modeling is that if we have a pretty good idea of what the opponent might do, we can beat them faster by playing moves which aren&rsquo;t objectively &ldquo;optimal&rdquo; as far as minimax is concerned. Here, Maia 1900 is a model of a relatively high-level chess player. Antimaia 1900 is specifically designed to counter Maia 1900.</p>
<table>
	<thead>
			<tr>
					<th>White</th>
					<th>Black</th>
					<th>Games</th>
					<th>White win %</th>
					<th>Average half moves per game</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Antimaia 1900</td>
					<td>Maia 1900</td>
					<td>100</td>
					<td>100%</td>
					<td>37</td>
			</tr>
			<tr>
					<td>Maia 1900</td>
					<td>Antimaia 1900</td>
					<td>100</td>
					<td>0%</td>
					<td>43</td>
			</tr>
			<tr>
					<td>Maia 1900</td>
					<td>Stockfish @10</td>
					<td>100</td>
					<td>0%</td>
					<td>67</td>
			</tr>
			<tr>
					<td>Stockfish @10</td>
					<td>Maia 1900</td>
					<td>100</td>
					<td>100%</td>
					<td>70</td>
			</tr>
	</tbody>
</table>
<ul>
<li>Maia 1900 uses Maia Chess weights trained on games from players rated near 1900.</li>
<li>Antimaia 1900 uses Maia 1900 weights and Stockfish @10.</li>
<li>Stockfish @10 is Stockfish 14.1 evaluated at depth 10.</li>
</ul>
<p>As you can see, Antimaia finishes its games much more quickly (12-16 moves faster!). This demonstrates that Antimaia is exploiting weaknesses in Maia&rsquo;s policy as measured by Stockfish, and playing high risk/high reward moves which usually pay off.</p>
<h2 id="selected-games">Selected Games</h2>
<p>I had to keep reminding myself that even though these games look ridiculous, Maia plays poorly because Antimaia is finding moves where Maia is most likely to mess up.</p>
<h3 id="antimaia-vs-maia">Antimaia vs Maia</h3>
<ul>
<li>
<p><a href="https://lichess.org/cRCrl2yO">Game 15</a>: Smothered mate comes out of nowhere.
<img src="https://lichess1.org/game/export/gif/white/cRCrl2yO.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/CIJn9rD0">Game 24</a>: Antimaia lays a poison pawn trap then waits patiently for 2 moves before Maia falls for it, blundering a queen.
<img src="https://lichess1.org/game/export/gif/white/CIJn9rD0.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/EySDK7IP">Game 32</a>: The shortest game. Another nice checkmate delivered by the knight.
<img src="https://lichess1.org/game/export/gif/white/EySDK7IP.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/kkRE06DB">Game 59</a>: Maia blunders a piece and then falls for Antimaia&rsquo;s poison rook to get checkmated.
<img src="https://lichess1.org/game/export/gif/white/kkRE06DB.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/qTJg2D26">Game 75</a>: Promoting to a knight not because it&rsquo;s the best move, but because it doesn&rsquo;t matter.
<img src="https://lichess1.org/game/export/gif/white/qTJg2D26.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/ERFLY3Wa">Game 88</a>: Antimaia makes the crazy move Bh7 but Maia blunders into checkmate by taking with the knight.
<img src="https://lichess1.org/game/export/gif/white/ERFLY3Wa.gif" alt="gif"></p>
</li>
</ul>
<h3 id="maia-vs-antimaia">Maia vs. Antimaia</h3>
<ul>
<li>
<p><a href="https://lichess.org/aCUt0XbV">Game 2</a>: Queen takes e3 is objectively a horrible move but Antimaia gambles correctly that Maia won&rsquo;t know how to respond.
<img src="https://lichess1.org/game/export/gif/black/aCUt0XbV.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/pgPCKq4E">Game 7</a>: Antimaia baits Maia into blundering a queen and mate in 3.
<img src="https://lichess1.org/game/export/gif/black/pgPCKq4E.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/DyBQrLwo">Game 51</a>: Antimaia tricks Maia with a poison pawn.
<img src="https://lichess1.org/game/export/gif/black/DyBQrLwo.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/25aF17bv">Game 59</a>: Maia manages to lose in 5 moves.
<img src="https://lichess1.org/game/export/gif/black/25aF17bv.gif" alt="gif"></p>
</li>
<li>
<p><a href="https://lichess.org/my88HT0H">Game 69</a>: Antimaia swindles a queen.
<img src="https://lichess1.org/game/export/gif/black/my88HT0H.gif" alt="gif"></p>
</li>
</ul>
<h2 id="the-rest-of-the-games">The rest of the games</h2>
<p>I added the rest of the games to github: <a href="https://github.com/lukesalamone/antimaia_games">https://github.com/lukesalamone/antimaia_games</a></p>
]]></content>
        </item>
        
        <item>
            <title>The Other End of the Earth</title>
            <link>https://blog.lukesalamone.com/posts/earth-antipodes/</link>
            <pubDate>Wed, 23 Nov 2022 10:07:36 -0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/earth-antipodes/</guid>
            <description>&lt;figure&gt;&lt;img src=&#34;../img/antipode_land.png&#34;
			alt=&#34;White areas show points of earth on land whose antipode is also on land. This is only about 8.6% of all of earth&amp;rsquo;s surface.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;White areas show points of earth on land whose antipode is also on land. This is only about 8.6% of all of earth&amp;rsquo;s surface.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;If you want to fly across the Pacific Ocean, you&amp;rsquo;ll have to board an airplane and fly around 12 hours. It&amp;rsquo;s pretty slow. A much faster route would be to go directly through the center of the earth. &amp;ldquo;Digging to China&amp;rdquo; was a common expression I heard growing up, with the implication that the opposite side of the globe is somewhere in Asia.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<figure><img src="/img/antipode_land.png"
			alt="White areas show points of earth on land whose antipode is also on land. This is only about 8.6% of all of earth&rsquo;s surface."><figcaption>
			<p>White areas show points of earth on land whose antipode is also on land. This is only about 8.6% of all of earth&rsquo;s surface.</p>
		</figcaption>
</figure>

<p>If you want to fly across the Pacific Ocean, you&rsquo;ll have to board an airplane and fly around 12 hours. It&rsquo;s pretty slow. A much faster route would be to go directly through the center of the earth. &ldquo;Digging to China&rdquo; was a common expression I heard growing up, with the implication that the opposite side of the globe is somewhere in Asia.</p>
<p>For the US, that&rsquo;s actually not true. The opposite end of the earth for most of the US is actually somewhere west of Australia. For North America, there are actually very few places whose antipode (the point on the exact opposite side of the globe) is on dry land. If you could dig a hole through the center of the earth from the US, you&rsquo;d pop out in the Pacific Ocean.</p>
<p>It is actually possible to &ldquo;dig to China&rdquo; though. Most of South America (excluding most of Brazil) has an antipode in east Asia. In fact, several of the world&rsquo;s biggest cities have antipodes on dry land. I&rsquo;ll get to this later.</p>
<h2 id="calculating-antipodes">Calculating Antipodes</h2>
<p>Given the latitude and longitude of a point on earth, it&rsquo;s pretty easy to calculate its antipode:</p>
<pre><code class="language-python">def antipode(lat, lon):
  lat = 0 - lat
  lon = 180 + lon if lon &lt; 0 else lon - 180
  return lat, lon
</code></pre>
<h2 id="dry-land">Dry land?</h2>
<p>Once I have the antipode of a particular latitude and longitude, I want to know if it&rsquo;s on dry land or not. The process of figuring out what&rsquo;s at a particular latitude/longitude point is called reverse geocoding, and there are a few nice tools for doing this for dry land. These tools will give fairly good information about the country a point is in.</p>
<p>But for now all I care about is whether a particular point is on land or water, not what country it&rsquo;s in. I wasn&rsquo;t able to find a great tool online that does this, so I opted for a more brute force method.</p>
<p>I used <a href="https://upload.wikimedia.org/wikipedia/commons/thumb/2/23/Blue_Marble_2002.png/2560px-Blue_Marble_2002.png">this picture from Wikipedia</a> which represents almost all water as the same color of blue. Based on that, I can create a boolean mask to indicate oceans and lakes, and reverse project pixels from that image to geographic coordinates. Simple enough.</p>
<h2 id="pixels-to-geocoordinates">Pixels to Geocoordinates</h2>
<p>Once you have the (x,y) coordinates they need to be converted to longitude and latitude. Latitude is how far north (or south) you are on the globe and ranges from -90 degrees at the North Pole to 90 degrees at the South Pole. On a flat map this is represented by a y coordinate.</p>
<p>Longitude represents how far east or west you are from the Prime Meridian (which runs through Greenwich, UK) and ranges from -180 to 180, both of which represent the same meridian.</p>
<p>How a latitude/longitude point is projected onto the globe depends on the map projection. In this case we&rsquo;re using <a href="https://en.wikipedia.org/wiki/Equirectangular_projection">Equirectangular projection</a>, so we just need to reverse that operation to get the x/y coordinates. The formula for doing so is:</p>
<pre><code class="language-python">#height is the height of the image
def lat2y(lat):
    return int((90 - lat) * (height / 180))

# width is the width of the image
def lon2x(lon):
    return int((lon + 180) * (width / 360))

# the reverse operations of the above
def y2lat(y):
    return 90 - 180 * y / height

def x2lon(x):
    return 360 * x / width - 180
</code></pre>
<h2 id="pixel-antipodes">Pixel Antipodes</h2>
<p>Now we can calculate the antipodes of a pixel.</p>
<pre><code class="language-python">def pixel_antipode(y, x):
    lat, lon = antipode(y2lat(y), x2lon(x))
    return lat2y(lat), lon2x(lon)
</code></pre>
<h2 id="antipodal-cities">Antipodal Cities</h2>
<p>If we look at <a href="https://en.wikipedia.org/wiki/List_of_largest_cities">a list of the world&rsquo;s largest cities</a>, which of them have antipodes on land? In the top 20, only four cities do.</p>
<table>
	<thead>
			<tr>
					<th>city</th>
					<th>population (millions)</th>
					<th>lat, lon</th>
					<th>antipode country</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Shanghai🇨🇳</td>
					<td>25.5</td>
					<td>31.2, 121.5</td>
					<td>Argentina🇦🇷</td>
			</tr>
			<tr>
					<td>Beijing🇨🇳</td>
					<td>19.6</td>
					<td>39.9, 116.4</td>
					<td>Argentina🇦🇷</td>
			</tr>
			<tr>
					<td>Manila🇵🇭</td>
					<td>13.5</td>
					<td>14.6, 121.0</td>
					<td>Brazil🇧🇷</td>
			</tr>
			<tr>
					<td>Tianjin🇨🇳</td>
					<td>13.2</td>
					<td>39.1, 117.2</td>
					<td>Argentina🇦🇷</td>
			</tr>
	</tbody>
</table>
]]></content>
        </item>
        
        <item>
            <title>Attention Walkthrough</title>
            <link>https://blog.lukesalamone.com/posts/attention-walkthrough/</link>
            <pubDate>Tue, 22 Nov 2022 23:35:04 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/attention-walkthrough/</guid>
            <description>&lt;p&gt;In this post we will walk through a very simple example of multiheaded attention is computed. The goal is to keep the dimensions small so that it&amp;rsquo;s easier to understand what&amp;rsquo;s going on. For a more conceptual explanation of attention, see &lt;a href=&#34;../posts/self-attention/&#34;&gt;A Few Notes on the Transformer&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;step-0-define-some-constants&#34;&gt;Step 0: Define some constants&lt;/h2&gt;
&lt;p&gt;We will use the three tokens &lt;code&gt;[&#39;major&#39;, &#39;league&#39;, &#39;baseball&#39;]&lt;/code&gt; with model dimension 4 (&lt;code&gt;d_model=4&lt;/code&gt;) and two attention heads (&lt;code&gt;num_heads=2&lt;/code&gt;).&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>In this post we will walk through a very simple example of multiheaded attention is computed. The goal is to keep the dimensions small so that it&rsquo;s easier to understand what&rsquo;s going on. For a more conceptual explanation of attention, see <a href="/posts/self-attention/">A Few Notes on the Transformer</a>.</p>
<h2 id="step-0-define-some-constants">Step 0: Define some constants</h2>
<p>We will use the three tokens <code>['major', 'league', 'baseball']</code> with model dimension 4 (<code>d_model=4</code>) and two attention heads (<code>num_heads=2</code>).</p>
<pre><code class="language-python">num_heads = 2
d_model = 4
d_k = d_v = d_model // num_heads
</code></pre>
<p>Here we also define the dimensions of the key and value here so that after after concatenation in the multihead attention algorithm, the final result will have dimension <code>(seq_len, d_model)</code>.</p>
<h2 id="step-1-get-token-embeddings">Step 1: Get token embeddings</h2>
<p>Usually we would tokenize the input string &ldquo;major league baseball&rdquo; and then look up each corresponding embedding in the model&rsquo;s token embedding table. Here, we will just use random numbers to illustrate the math.</p>
<pre><code class="language-python">major = array([0.4, 0.2, 0.1, 0.3])
league = array([0.5, 0.3, 0. , 0.9])
baseball = array([0.8, 0.4, 0.2, 0.6])

x = np.stack([major, league, baseball])
</code></pre>
<h2 id="step-2-define-weight-matrices">Step 2: Define weight matrices</h2>
<p>Here we define four weight matrices:</p>
<ul>
<li>The query, key, and value weight matrices have dimensions <code>(n_heads, d_model, d_k)</code>. It is pretty standard for <code>d_k == d_v</code>, although it is technically not mathematically required to be the case.</li>
<li>The output projection matrix is typically a square matrix with dimension <code>(d_model, d_model)</code>.</li>
</ul>
<pre><code class="language-python">wq = array([
    [
        [0.5, 0.6],
        [0.8, 0.3],
        [0.7, 0.2],
        [0.1, 0.8]
    ],
    [
        [0.3, 0.6],
        [0.5, 0.8],
        [0.9, 0.1],
        [0.5, 0.8]
    ]
])

wk = array([
    [
        [0. , 0.7],
        [0.4, 0.3],
        [0.8, 0.3],
        [0.7, 0.4]
    ],
    [
        [0.4, 0.9],
        [0.1, 0.7],
        [0.6, 0.6],
        [0.1, 0.6]
    ]
])

wv = array([
    [
        [0.4, 0.5],
        [0.7, 0.6],
        [0.9, 0.8],
        [0. , 0.3]
    ],
    [
        [0.9, 0.2],
        [0.2, 0.8],
        [0.7, 0. ],
        [0.1, 0.4]
    ]
])

wo = array([
    [0.2, 0.2, 0.6, 0.1],
    [0.3, 0.2, 0.2, 0.8],
    [0.2, 0.5, 0.1, 0.3],
    [0.6, 0.9, 0.5, 0.7]
])
</code></pre>
<h2 id="step-3-the-multi-head-attention-algorithm">Step 3: The multi-head attention algorithm</h2>
<p>Now that we have our pieces ready, we can compute multihead attention on the input. The following is an example to illustrate the mechanism of multihead attention. Real implementations are vectorized to avoid slow loops.</p>
<pre><code class="language-python"># the input x has shape (seq_len, d_model)
def mha(x):
    heads = []
    for h in range(num_heads):
        q = x@wq[h]
        k = x@wk[h]
        v = x@wv[h]
        head = sdpa(q, k, v) # head has shape (seq_len, d_v)
        heads.append(head)
    concat = np.concatenate(heads, axis=1)
    return concat @ wo
</code></pre>
<p>As you can see, for each head we:</p>
<ul>
<li>compute the query matrix by matmul of input x with the query weights for that head. Notice that in this operation the input <code>x</code> has shape <code>(seq_len, d_model)</code> and the query weights for that head will have shape <code>(d_model, d_k)</code>, so the resulting matrix will have shape <code>(seq_len, d_k)</code>.</li>
<li>compute the key and value matrices in the same way</li>
<li>compute the final value of the head after scaled dot product attention between that head&rsquo;s query, key, and value matrices. We will talk about SDPA in the next step</li>
<li>The final value of the head will have shape <code>(seq_len, d_v)</code>. If we think of these dimensions as rows and columns, the &ldquo;head&rdquo; is really operating on a group of columns to make a &ldquo;skinny&rdquo; matrix.</li>
</ul>
<p>Finally, we concatenate all heads over the last dimension. This combines all &ldquo;skinny&rdquo; head matrices into a final matrix with shape <code>(seq_len, d_model)</code>. We then matmul that combined matrix by the out projection matrix.</p>
<h2 id="step-4-scaled-dot-product-attention">Step 4: Scaled dot product attention</h2>
<p>Scaled dot product attention here is the same as in single-head attention (i.e. self-attention). The difference here is that we are sending &ldquo;skinnier&rdquo; matrices into the SPDA function.</p>
<pre><code class="language-python">def sdpa(q, k, v):
    raw_attn_scores = q@k.T
    norm_attn_scores = raw_attn_scores / d_k ** 0.5
    weights = np.stack([
        softmax(row)
        for row in norm_attn_scores
    ])
    return weights @ v
</code></pre>
<p>First, we matmul the query by the transpose of the key. Both the query and key matrices have dimension <code>(seq_len, d_v)</code>, so <code>raw_attn_scores</code> will have dimension <code>(seq_len, seq_len)</code>.</p>
<p>Next, we divide <code>raw_attn_scores</code> by the square root of <code>d_k</code>. This helps to stabilize training since the variance here scales with <code>d_k</code>, specifically in the softmax in the next step.</p>
<p>After that, we compute the softmax values for each row. Since each corresponds to a token in the input, this can be thought of as emphasizing which tokens in the sequence are most relevant to each query token. This operation doesn&rsquo;t change the input shape.</p>
<p>Finally, we matmul the weights with the value matrix, <code>(seq_len, seq_len) x (seq_len, d_v)</code> resulting in a final matrix of <code>(seq_len, d_v)</code>.</p>
]]></content>
        </item>
        
        <item>
            <title>A Few Notes on the Transformer</title>
            <link>https://blog.lukesalamone.com/posts/self-attention/</link>
            <pubDate>Wed, 16 Nov 2022 15:24:15 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/self-attention/</guid>
            <description>&lt;p&gt;&lt;em&gt;For a more mechanical explanation of the self-attention and multi-headed attention calculation, see the &lt;a href=&#34;../posts/attention-walkthrough/&#34;&gt;Attention Walkthrough&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;../img/self-attention.png&#34;
			alt=&#34;A self-attention block depicted as a neural network.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;A self-attention block depicted as a neural network.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;In this post I will describe the attention mechanism, commonly used in transformers, a popular neural language architecture. Most of the most well-known large language models of late are based on the transformer architecture. Attention was first described in &lt;a href=&#34;https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf&#34;&gt;Attention is All You Need&lt;/a&gt; by Vaswani et al.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><em>For a more mechanical explanation of the self-attention and multi-headed attention calculation, see the <a href="/posts/attention-walkthrough/">Attention Walkthrough</a>.</em></p>
<figure><img src="/img/self-attention.png"
			alt="A self-attention block depicted as a neural network."><figcaption>
			<p>A self-attention block depicted as a neural network.</p>
		</figcaption>
</figure>

<p>In this post I will describe the attention mechanism, commonly used in transformers, a popular neural language architecture. Most of the most well-known large language models of late are based on the transformer architecture. Attention was first described in <a href="https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf">Attention is All You Need</a> by Vaswani et al.</p>
<h2 id="what-is-attention">What is attention?</h2>
<p>At a high level, attention is a mechanism for neural networks to boost portions of an input which are relevant and ignore those which aren&rsquo;t. In language models, attention is used as a way for the model to learn which portions of a sentence are relevant to each word.</p>
<h2 id="what-is-attention-for">What is attention for?</h2>
<p>Let&rsquo;s use an example:</p>
<blockquote>
<p>I am sitting at the library with my friend.</p>
</blockquote>
<p>It should be pretty clear that not all words in this sentence are equally important. What words are relevant to &ldquo;I&rdquo;? Probably &ldquo;sitting&rdquo;, &ldquo;library&rdquo;, and &ldquo;friend&rdquo;. Likewise, &ldquo;the&rdquo; might only be relevant to &ldquo;library&rdquo;. Attention provides a way for a model to increase and decrease the importance of each word.</p>
<p>Since the value of each token in the sequence is dependent on other tokens, this method of generating word embeddings is very different from more classical methods like <a href="https://arxiv.org/pdf/1301.3781.pdf">Word2Vec</a> and <a href="https://nlp.stanford.edu/projects/glove/">GloVe</a>. There is no one fixed vector for a given word.</p>
<p>And this makes sense. Many words in English are homonyms, and have identical spellings for distinct meanings. For example, &ldquo;rock&rdquo; is a genre of music but also can mean a stone. <a href="https://www.npr.org/2011/05/30/136796448/has-run-run-amok-it-has-645-meanings-so-far">The word &ldquo;run&rdquo; has 645 meanings and has recently replaced &ldquo;set&rdquo; as the word with the most definitions.</a> It would not make sense for all of these homonyms to have the same vector.</p>
<h2 id="an-interactive-example">An interactive example</h2>
<p>You can hover over each word to see the relative importances of each word in the sentence to the hovered word.</p>
<div id="attention_demo"></div>
<link rel="stylesheet" href="/css/attention-demo.css" />
<script src="/js/attention_demo.js"></script>
<h2 id="how-does-self-attention-work">How does self-attention work?</h2>
<p><figure><img src="/img/scaled-dot-prod-attention.png"
			alt="The Vaswani paper describes scaled dot product attention, which involves normalizing by the square root of the input dimension."><figcaption>
			<p>The Vaswani paper describes scaled dot product attention, which involves normalizing by the square root of the input dimension.</p>
		</figcaption>
</figure>

This is the part where Vaswani delves into a database analogy with <strong>keys</strong>, <strong>queries</strong>, and <strong>values</strong>. Most online resources try to salvage this analogy. Personally, I always found this a bit confusing. What you need to know is that keys, values, and queries correspond to 3 matrices M<sub>k</sub>, M<sub>q</sub>, and M<sub>v</sub>, which are used in a dot product with the original input vectors.</p>
<p>In linear algebra terms this means multiplying the 1xd input vector by a matrix of size dxd. In neural network terms, this means passing the input vector through a full-connected layer. After M<sub>k</sub> and M<sub>q</sub> are multiplied, they are normalized by the square root of d<sub>k</sub>, a constant representing the dimension of the input vector.</p>
<h2 id="can-we-attend-to-multiple-parts-of-a-sentence">Can we attend to multiple parts of a sentence?</h2>
<figure><img src="/img/multi_head_attention.png"
			alt="Multi-headed attention means performing attention n times in parallel inside of an encoder block."><figcaption>
			<p>Multi-headed attention means performing attention n times in parallel inside of an encoder block.</p>
		</figcaption>
</figure>

<p>Yes, that is called multi-headed attention. Its architecture is very similar, using  additional M<sub>k</sub>, M<sub>q</sub>, and M<sub>v</sub> matrices for each additional &ldquo;attention head&rdquo;. In the Vaswani paper they used 8 heads.</p>
<h2 id="how-do-transformers-compare-with-other-architectures-eg-rnncnn">How do transformers compare with other architectures (e.g. RNN/CNN)?</h2>
<figure><img src="/img/attention_performance.png"
			alt="When the input sequence length n is lower than the input dimensionality d, self-attention is faster than recurrent neural networks. Self-attention is also easily parallelizable."><figcaption>
			<p>When the input sequence length n is lower than the input dimensionality d, self-attention is faster than recurrent neural networks. Self-attention is also easily parallelizable.</p>
		</figcaption>
</figure>

<p>Generally speaking, RNNs are able to memorize but not parallelize, and CNNs are able to parallelize but not memorize. Transformers are able to do both.</p>
<p>The Vaswani paper outlines three main benefits:</p>
<ol>
<li>
<p>Computational complexity per layer. Self-attention layers are faster than recurrent layers when the input sequence length is smaller than the input vector dimensionality.</p>
</li>
<li>
<p>The opportunity to parallelize calculations. Each head in multi-headed attention can be computed separately in an encoder layer.</p>
</li>
<li>
<p>Easier to learn long-range dependencies. For many English sentences, especially fairly complex ones found in more scientific writings, the full context of a word cannot be learned from its immediate neighbors. Sometimes it can&rsquo;t even be found in the same sentence. However, even though most language models prior to the transformer had theoretically infinite input sequence lengths, in practice it was quite difficult for them to learn long-range dependencies. Because a transformer sees its whole input simultaneously, Vaswani argues, it is able to more easily learn those dependencies.</p>
</li>
</ol>
<h2 id="is-that-all">Is that all?</h2>
<p>No. In part two I will describe the encoder and decoder blocks, as well as the self-supervised training process.</p>
]]></content>
        </item>
        
        <item>
            <title>Rolling My Own Blog Search</title>
            <link>https://blog.lukesalamone.com/posts/rolling-my-own-blog-search/</link>
            <pubDate>Wed, 09 Nov 2022 02:42:51 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/rolling-my-own-blog-search/</guid>
            <description>&lt;p&gt;I&amp;rsquo;ve found myself hitting ctrl+f on this blog enough that I figured it&amp;rsquo;s about time to add some search functionality to it. While there are certainly prefab solutions out there, this task is simple enough and fairly instructive. I had a few requirements, though:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The search needs to be fast, useful, and aesthetically pleasing.&lt;/li&gt;
&lt;li&gt;Search in the browser. Standing up a server is a lot of extra work. It&amp;rsquo;s also overkill since I only have about 30 articles so far.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;semantic-search&#34;&gt;Semantic search&lt;/h2&gt;
&lt;p&gt;I ran some experiments with small neural networks deployed using ONNX but ultimately they didn&amp;rsquo;t seem to be a good fit for this blog. The search experience was not quite as snappy as I&amp;rsquo;d have liked it to be, and while I was able to get the model under 10MB, it still added a good amount of bloat to the page size. Further, it wasn&amp;rsquo;t clear to me that the search results were significantly better, and in some cases they were worse. In any case, the advantages were not enough to justify the added page size.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>I&rsquo;ve found myself hitting ctrl+f on this blog enough that I figured it&rsquo;s about time to add some search functionality to it. While there are certainly prefab solutions out there, this task is simple enough and fairly instructive. I had a few requirements, though:</p>
<ol>
<li>The search needs to be fast, useful, and aesthetically pleasing.</li>
<li>Search in the browser. Standing up a server is a lot of extra work. It&rsquo;s also overkill since I only have about 30 articles so far.</li>
</ol>
<h2 id="semantic-search">Semantic search</h2>
<p>I ran some experiments with small neural networks deployed using ONNX but ultimately they didn&rsquo;t seem to be a good fit for this blog. The search experience was not quite as snappy as I&rsquo;d have liked it to be, and while I was able to get the model under 10MB, it still added a good amount of bloat to the page size. Further, it wasn&rsquo;t clear to me that the search results were significantly better, and in some cases they were worse. In any case, the advantages were not enough to justify the added page size.</p>
<p>I learned a lot with this experiment which probably deserves a blog post of its own at some point. I might revisit semantic matching at a later point as well. (Post script: <a href="/posts/creating-tiny-semantic-search/#extending-techniques-to-blog-search">I did</a>.)</p>
<h2 id="bm25">BM25</h2>
<p>Rather than loading a neural network, I decided instead to index the text of my blog and use <a href="https://en.wikipedia.org/wiki/Okapi_BM25">BM25</a> to rank documents.</p>
<p>One difficulty in this was finding the actual algorithm which didn&rsquo;t include a ton of extra code. I was only interested in the function itself, not all of the extra boilerplate code floating around. In the end I broke down and rewrote it myself.</p>
<h2 id="tokenizing-text">Tokenizing Text</h2>
<p>BM25 works by scoring document tokens against query tokens. Documents with more relevant tokens should be scored higher than those with fewer relevant tokens.</p>
<p>This raises the question of how to convert a query string into a group of tokens. Initially, I simply split by word. That is,</p>
<pre><code>&quot;hello my friend&quot; =&gt; ['hello', 'my', 'friend']
</code></pre>
<p>Although this works pretty well, it has the downside that different forms of a word will be missed. For example &ldquo;friends&rdquo; will not match with &ldquo;friend&rdquo;. To solve this problem, I decided to use fixed token sizes of three characters instead:</p>
<pre><code>&quot;hello my friend&quot; =&gt; ['hel', 'ell', 'llo', 'lo#', '#my', 'my#', '#fr', 'fri', 'rie', 'ien', 'end']
</code></pre>
<p>In this way &ldquo;friend&rdquo; will be tokenized very similarly to &ldquo;friends&rdquo;, so the penalty for mismatches between forms of words will be minimized. Subjectively, I felt that this yielded a much more fluid and versatile ranking as well.</p>
<h2 id="multi-factor-scoring">Multi-factor scoring</h2>
<p>My blog posts have title, body, and tags. I could simply concatenate them all together and score the combined string, and it would work pretty well. But this seems to throw away the fact that the title and tags may have more concentrated information than the article body does. They deserve to be weighted differently. I decided to weight the title/body/tags components as 30/50/20 in the final ranking.</p>
]]></content>
        </item>
        
        <item>
            <title>A new type of chess tournament</title>
            <link>https://blog.lukesalamone.com/posts/qualitative-analysis-chess/</link>
            <pubDate>Sat, 08 Oct 2022 15:17:36 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/qualitative-analysis-chess/</guid>
            <description>&lt;p&gt;&lt;em&gt;&lt;strong&gt;This is part 2 of a paper I wrote for &lt;a href=&#34;https://www.mccormick.northwestern.edu/research-faculty/directory/profiles/forbus-ken.html&#34;&gt;Ken Forbus&lt;/a&gt;&amp;rsquo; Qualitative Reasoning course, adapted for this blog. You can find a printable version of the paper &lt;a href=&#34;../files/anthropomorphic-chess-evaluation-via-qualitative-analysis.pdf&#34;&gt;here&lt;/a&gt; and part 1 &lt;a href=&#34;../posts/chess-engine-history/&#34;&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In the previous post I discussed the history of chess engines and why they don&amp;rsquo;t &amp;ldquo;think&amp;rdquo; like we think. Trading interpretability for computation cycles ultimately led to the engines we have today, fairly alien in nature and perhaps less pedagogically useful because of it. At the time, though, the goal was to beat human grandmasters by any means necessary, a great engineering feat that the field had been working on for decades.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><em><strong>This is part 2 of a paper I wrote for <a href="https://www.mccormick.northwestern.edu/research-faculty/directory/profiles/forbus-ken.html">Ken Forbus</a>&rsquo; Qualitative Reasoning course, adapted for this blog. You can find a printable version of the paper <a href="/files/anthropomorphic-chess-evaluation-via-qualitative-analysis.pdf">here</a> and part 1 <a href="/posts/chess-engine-history/">here</a>.</strong></em></p>
<p>In the previous post I discussed the history of chess engines and why they don&rsquo;t &ldquo;think&rdquo; like we think. Trading interpretability for computation cycles ultimately led to the engines we have today, fairly alien in nature and perhaps less pedagogically useful because of it. At the time, though, the goal was to beat human grandmasters by any means necessary, a great engineering feat that the field had been working on for decades.</p>
<p>This post contains two related proposals. The first is a chess engine tournament, unique in the type of engine which will be permitted to enter and likely to succeed. Importantly, the vast majority of engines currently holding the highest performance ratings will likely not be effective.</p>
<p>The second proposal is the outlines of a chess engine that is likely to be successful in this tournament, taking advantage of the highly qualitative nature of chess position evaluation. Although it is unlikely to perform as strongly against top-performing engines, there are several distinct advantages of such an engine. In short, there is likely to be a great deal of educational value as well as financial incentive driving the construction of highly successful qualitative chess engines.</p>
<h1 id="qualitative-chess-analysis">Qualitative chess analysis</h1>
<p>Qualitatively, there are many aspects to a chess game that may be captured. Let&rsquo;s take a look at the way a grandmaster analyzes a position. It will become quite apparent that at the highest levels, the qualitative aspects of position analysis dominate over <a href="/posts/chess-blunders/">quantitative aspects</a> (i.e. the number and value of each piece).</p>
<p>In the selected lecture, Grandmaster Varuzhan Akobian details a game he played previously. At a key moment of the game, Akobian sacrificed his rook for a key pawn in the center of the board. The resulting position is reproduced for convenience in Figure 1.</p>
<figure><img src="/img/human_analysis_fig4.png"
			alt="Figure 1. Quantitative analysis would posit that black is winning due to his extra rook for white’s knight and pawn. However, qualitative analysis provides a more complete picture of black’s predicament."><figcaption>
			<p>Figure 1. Quantitative analysis would posit that black is winning due to his extra rook for white’s knight and pawn. However, qualitative analysis provides a more complete picture of black’s predicament.</p>
		</figcaption>
</figure>

<p><a href="https://youtu.be/h80Mu4N6oYI?t=1515">His analysis starts at 25:15 in the video</a>:</p>
<blockquote>
<p>I would like you to spend a minute or two just to give me the evaluation of this position. It may not seem that clear because I’m down the exchange. [Novice chess players are taught that chess pieces have quantitative values, which may come into consideration when exchanging one piece for another. These values are measured in terms of pawns. Knights and bishops are generally understood to be worth 3 pawns, rooks are worth 5, and queens are worth 9.] I have a knight and a pawn for a rook. Rook is valued 5, knight and a pawn is 4. It may seem like I’m down a pawn here. But what do you think is the proper evaluation of this position?</p>
</blockquote>
<blockquote>
<p>&hellip;Basically white is very active. There are a few other things we can mention about white’s position, that it’s very strong. What else is very strong? White’s king is very safe, he cannot attack me. But how about the black king? Do you think the black king is very safe? [No.] For example, I could put my queen here [the e4 square] then I have a battery! Remember when we have a queen and a bishop on the same diagonal we call that a battery. And suddenly if I can deflect this queen [black queen on the g7 square] I will just go queen takes pawn, checkmate!</p>
</blockquote>
<blockquote>
<p>His dark square bishop is basically trapped behind his own pawns so it’s ineffective. . . . My bishop is very active. . . . And one more thing that you can mention. Passed pawn, exactly! And it’s a very strong passed pawn because with a knight on d6 very quickly [the pawn] will turn into [a queen].</p>
</blockquote>
<blockquote>
<p>. . . How much advantage does white have here? Big advantage, slight advantage, maybe winning? . . . We’re not going to use Houdini [a chess engine], Houdini will probably say black is slightly worse. But in practical play, I would be very comfortable to play this against anybody, and pretty comfortable I can win this position for white.</p>
</blockquote>
<p>Note that <a href="/posts/chess-blunders/">quantitative analysis</a> is almost entirely absent from GM Akobian’s evaluation. Towards the beginning he mentions that he has sacrificed his rook for a knight and a pawn, and consequently is at a material deficit. However, he quickly discards this shallow evaluation, going so far as to label his subsequent qualitative evaluation as the “proper” evaluation.</p>
<p>GM Akobian goes on to mention several other qualitative features of the position which are difficult to assign quantitative value to. Firstly, the activity of his pieces means that it is much easier to play the position as white because his pieces are on better squares, including some deep in black’s half of the board. The lack of activity is mentioned later on, noting that black’s bishop is essentially trapped behind his own pieces.</p>
<p>King safety is another difficult thing to quantify. In the given position, it is difficult to find a way that black can even check the white king. Moving the d8 rook to b1 will take 2 moves, and even then the b1 square is guarded by the bishop on d3. So the white king is indeed quite safe. In contrast black king is quite vulnerable, guarded mainly by the black queen, who is herself vulnerable to deflection or direct attack. (Deflection is a chess tactic which involves “distracting” an opponent’s piece which plays an important defensive role. For example, a piece which is defending two pieces simultaneously may be deflected by capturing one of the defended pieces.)</p>
<p>GM Akobian emphasizes the weakness of black’s king by sketching out a simple game-winning checkmate plan: arrange the bishop and queen in a battery which attacks the h7 pawn, deflect the black queen, and deliver checkmate with the queen by taking the h7 pawn. Although it is not immediately clear how to implement the plan, this type of simple plan creates a well-defined long-term “threat” that black must contend with.</p>
<p>Another threat he mentions is encompassed by white’s passed pawn on c5. This pawn may become a queen, which would become an insurmountable advantage for white. Therefore, this threat is another long-term vulnerability for black. (A passed pawn is a pawn which cannot be stopped or attacked by an opponent’s pawns. This occurs when there are no opponent pawns in the “file” (vertical column) of the pawn, as well as the file to the left and the right, if applicable. For example, a pawn in the C file is a passed pawn if there are no opponent pawns in the B, C or D files. A pawn in the H file is passed if there are no opponent pawns in the G or H files.)</p>
<p>Finally, note that GM Akobian does not assign a quantitative value to the board position, but rather a “very comfortable to win” assessment. Very little of this analysis involves quantities, but rather qualitative situations which must be dealt with. Consequently, it seems that qualitative reasoning is an ideal tool which a chess engine might use.</p>
<h2 id="qualitative-analysis-is-more-human">Qualitative analysis is more human</h2>
<p>The nature of expert-level perception described by GM Akobian was studied directly in a <a href="https://drive.google.com/file/d/1gvAMm39EVVN9bPnWVOdrAXU_MwDqkf0Z/view?usp=sharing">1973 paper</a> by Chase and Simon. Participants at three different levels of chess ability (a master-level player, an experienced club-level player, and a novice) were asked to complete two chess-related cognitive tasks. The first was a perception task, requiring him to reproduce a chess position on an adjacent board as quickly as possible, with the model board in plain view. The second task was a memory task, requiring participants to reproduce a position from memory after viewing it for only 5 seconds.</p>
<p>Importantly, the perceptual study attended to chess players’ tendencies to “chunk” the board position as they reproduced positions, tending to remember groups of interrelated pieces. These pieces tended to have relationships which the authors characterized in five ways: a piece attacks another, a piece defends another, two pieces are adjacent, two pieces are the same color, two pieces are the same type.</p>
<p>The results of the study found that “the C, S, and null relations are low because subjects are placing pieces which usually have multiple relations. Thus, from the within-glance relations, it appears that subjects are noticing the pawn structure, clusters of pieces of the same color, and attack and defense relations over small spatial distances.” In other words, it seems likely that human players use the qualitative relationships between pieces to remember the board.</p>
<h1 id="a-qualitative-chess-engine">A qualitative chess engine</h1>
<p>It is unlikely that a qualitative chess engine will be able to entirely do away with the basic structural algorithm involved in chess calculations, i.e. minimax. We would like our qualitative engine to calculate in a way most similar to humans, and thus will require some level of ply depth to the calculations. However, a qualitative engine will have a much stronger sense of the “flow” of the game, and will thus explore fewer branches. Rather than considering each position as discrete, a qualitative engine should note how each move guides the evolution of the chess board position.</p>
<p>It is important to note that a qualitative chess engine may not be the most computationally efficient, a factor which was the primary motivation during the period of time when top chess engines needed to be run on supercomputers and every ounce of performance needed to be squeezed out of the machine. A qualitative engine should instead favor explainability over performance whenever possible. Instead, an engine should produce a good explanation of which moves were considered and why a particular move was chosen.</p>
<p>More modern qualitative research can improve upon Wilkins’ knowledge-based PARADISE approach. It is important to recognize that his knowledge base is quite similar in nature to the FAC component of the retrieval model <a href="https://groups.psych.northwestern.edu/gentner/papers/GentnerForbus91.pdf">presented</a> by Forbus et al. in 1995, but does not take advantage of the performance speedups presented there. Because of the high number of positional examples <a href="https://database.lichess.org/#puzzles">available online</a>, there is a huge opportunity for a performant analogical retrieval system at present. The MAC/FAC retrieval system could pay huge performance dividends in retrieval if applied to this problem.</p>
<p>Specifically, the Lichess database referenced above contains 1,737,489 chess “puzzles” as of the time of writing. A chess puzzle is simply a chess board position in which players are encouraged to find the best move. Each puzzle relates to one or more chess “themes” (e.g. “mate in 1”, “pin”, “discovered attack”, etc.), analogous to Wilkins’ concepts outlined above. Each puzzle also includes the best move to make in the position. Some research will need to be done to derive meaning from this best move, relating it by analogy to the current position being evaluated by the engine.</p>
<p>Qualitative spatial calculi may also be used to construct more psychologically plausible models of chess positions than simply noting which pieces occupy which squares, seeking to emulate the models suggested by Chase and Simon. Chess pieces have intricate relationships which can be captured, and which change whenever a piece moves to another square. Importantly, however, not all relationships are affected by the movement of a single chess piece, suggesting that performance gains may be realized by recomputing only those relationships which have changed.</p>
<p>It is likely that low-level piece relationships may give rise to higher level relationships and tactics. For example, the concept of “capturing the defender” arises from the concept of attacking a piece A which defends B, which works when pieces A and B are attacked by pieces C and D of opposing colors. And in the case of Figure 2 below, a defender may be “deflected” to win the piece it is defending. Defensive relationships may be thought of in a chain or directed graph, with each piece defending another and the safety of a piece being considered in relation to its connection to a defensive group.</p>
<figure><img src="/img/human_analysis_fig5.png"
			alt="Figure 2. White to move. From this image, many basic piece relationships are apparent with only 8 pieces left on the board. Importantly, the black king is defending the black queen. The black queen is also being attacked by the white queen, and is attacking the white queen. White’s queen is undefended, a state sometimes referred to as hanging. These relationships reveal the opportunity for a tactic. White can simultaneously move his rook to attack black’s king (danger levels) and take advantage of the defensive connection between the black king and queen with the move rook to b8. This forces black to move his king, removing the defense of his queen. In this position, black’s queen can be freely captured by white’s queen. Due to the mobility advantages of a queen over a rook, this is a favorable move sequence for white."><figcaption>
			<p>Figure 2. White to move. From this image, many basic piece relationships are apparent with only 8 pieces left on the board. Importantly, the black king is defending the black queen. The black queen is also being attacked by the white queen, and is attacking the white queen. White’s queen is undefended, a state sometimes referred to as hanging. These relationships reveal the opportunity for a tactic. White can simultaneously move his rook to attack black’s king (danger levels) and take advantage of the defensive connection between the black king and queen with the move rook to b8. This forces black to move his king, removing the defense of his queen. In this position, black’s queen can be freely captured by white’s queen. Due to the mobility advantages of a queen over a rook, this is a favorable move sequence for white.</p>
		</figcaption>
</figure>

<h2 id="applications-of-a-qualitative-chess-engine">Applications of a qualitative chess engine</h2>
<p>There are many benefits to reopening the pursuit of qualitative reasoning in chess. The first and most clear value proposition is that qualitative reasoning is likely to serve as a more plausible model for how humans think about the game. This is evidenced by the fact that as Chase and Simon found, chess players do not “see” the whole board at once, but rather in chunks of interrelated pieces. Even if the details of human mental models differ slightly from the implementation of a qualitative reasoning engine, it will be able to provide a traceable account of its decision-making process, an important step towards explainability.</p>
<p>As we saw in <a href="/posts/chess-engine-history/">part 1</a>, current top chess engines reason about chess in ways that are quite contrary to human intuition. Stockfish uses full-width search, considering each move in each position without prejudice and assigning numerical values to each position. As we saw from the analysis from GM Akobian, qualitative evaluations are far more meaningful to humans.</p>
<p>ther chess engines approach chess in an even more alien way. Specifically, it is unlikely that any engine which makes heavy use of neural evaluation functions will model human-derived organic strategies in ways which chess players will recognize. At the far end of this spectrum is the fully neural Maia chess engine, but even Lc0’s Monte-Carlo tree search precludes consideration for cognitive plausibility.</p>
<p>Qualitative chess engines which are able to better reproduce the types of chess reasoning used by top human chess players are also likely to serve as better pedagogical tools for those interested in studying chess. This applies at every level, from beginner to grandmaster. The skill level of such a chess engine would be quite easily tunable simply by disabling more advanced knowledge from the knowledge base.</p>
<p>This is a far more natural method of “handicapping” than the search depth limitations used in current chess engines. Each piece of knowledge becomes a tunable parameter to the engine. As students learn concepts, the corresponding representations in the knowledge base could be enabled, allowing for gradual learning in a far more accessible way. In fact, it is likely true that a qualitative chess engine could outperform human grandmasters (who often teach chess to others) in this respect.</p>
<p>Finally, it is likely that a qualitative engine would become a key component of a first line of defense against cheating in chess. Most cheating is performed by using assistance from a chess engine during online games with unsuspecting opponents. Consulting a functionally omniscient computer program can thus provide a cheater with a theoretically insurmountable advantage.</p>
<p>In an <a href="https://youtu.be/bmIFdrUVHXw?t=2280">interview</a> with the Perpetual Chess Podcast, Chris Callahan of the popular chess website LiChess.org stated that the majority of employees of the website work primarily to detect cheaters and yet the problem still persists. By exploiting the difference between conventional full-width engines like Stockfish and a qualitative evaluation, those working to detect cheaters will be better equipped to detect “suspicious” moves. However, qualitative chess engines are unlikely to be able to completely replace human moderation.</p>
<h1 id="nerf-the-engine">Nerf the Engine!</h1>
<p>A computer chess tournament be held between chess engines can encourage the type of reasoning and gameplay which resembles human games. However, because we are not interested in the best overall chess engine, but one which can reason like a human might, the rules of the tournament will be adjusted in several key ways to discourage brute-force computational methods. We already know that calculating millions of positions can find the optimal move. But what happens when an engine is limited to e.g. 1000 positions?</p>
<p>Because we expect few entrants in early iterations of this special tournament, engineering an automatic enforcement mechanism for the limitations stipulated in this document are likely to be unnecessary. Engine compliance may simply be verified through manual inspection. Future iterations may include further safeguards, potentially separating the position evaluation function and directly counting the number of invocations while arbitrating the tournament to directly verify compliance.</p>
<h2 id="position-limitation">Position limitation</h2>
<p>Firstly, competing chess engines will be limited in the number of board positions they can evaluate during any one move. Because human grandmasters evaluate around 100 positions before making a move, the tournament arbitration system will artificially impose this limitation on all competing engines.</p>
<p>This cap immediately creates an issue for full-width chess engines because of chess’ high branching factor. Were an engine to evaluate each possible move, it would perform quite poorly in board positions with many possible plies and replies available, rarely reaching a depth of more than 2 or 3. As a result, any engine which naively assesses a chess board would perform quite poorly in this setup.</p>
<p>The practical upshot of the position limitation is that the engine will be incentivized to gather as much relevant information about a position as possible rather than optimizing for the maximum number of positions.</p>
<h2 id="position-saliency">Position saliency</h2>
<p>Additionally, engines will be required to implement scheduling logic which takes the time remaining into consideration. While this creates the immediate problem of how an engine should allocate its time, it creates the ancillary challenge of evaluating a position’s quiescence. Positions which are “quiet” and have few forcing moves require less evaluation than positions in which there are many non-forcing moves.</p>
<p>This requirement immediately motivates a qualitative chess engine to recognize the futility of falling prey to the Horizon Effect. The Horizon Effect causes engines to waste many position calculations pursuing delaying moves which amount to hopeless rabbit trails. Instead, an engine should recognize that quiescence has to do with the <em>relationships between pieces</em>. Humans understand this and can quite quickly see the futility of a move and terminate their search. A qualitative analysis which takes this factor into consideration will be able to save a great deal of position calculations, behaving more like a human player.</p>
<h1 id="conclusion">Conclusion</h1>
<p>Given that computers have achieved and sustained superhuman capabilities in the domain of chess, the next frontier is not in building increasingly strong engines, but harnessing the present computational power to reason about the game in ways that humans do. Qualitative reasoning can provide novel and intuitive ways to reason about previously seen moves and think about the game.</p>
]]></content>
        </item>
        
        <item>
            <title>The Chess Engine&#39;s Final Horizon</title>
            <link>https://blog.lukesalamone.com/posts/chess-engine-history/</link>
            <pubDate>Fri, 07 Oct 2022 20:17:21 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/chess-engine-history/</guid>
            <description>&lt;p&gt;&lt;em&gt;&lt;strong&gt;This is part 1 of a paper I wrote for &lt;a href=&#34;https://www.mccormick.northwestern.edu/research-faculty/directory/profiles/forbus-ken.html&#34;&gt;Ken Forbus&lt;/a&gt;&amp;rsquo; Qualitative Reasoning course, adapted for this blog. You can find a printable version of the paper &lt;a href=&#34;../files/anthropomorphic-chess-evaluation-via-qualitative-analysis.pdf&#34;&gt;here&lt;/a&gt; and part 2 &lt;a href=&#34;../posts/qualitative-analysis-chess/&#34;&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Computers that play chess, otherwise known as chess engines, have existed &lt;a href=&#34;https://www.youtube.com/watch?v=wrxdWkjmhKg&#34;&gt;since at least the late 1940s&lt;/a&gt;. Because the game was said to require the perfect combination of planning, strategy, psychology, and calculation, chess was once thought to be an activity directly correlated with intelligence, and that only a truly intelligent computer should be able to defeat humans. However, as a recent chess.com &lt;a href=&#34;https://drive.google.com/file/d/11IokKgTVSXdpYEzAuyViIleSZ_2wl0ag/view&#34;&gt;report&lt;/a&gt; explains, computers are now far stronger than humans:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><em><strong>This is part 1 of a paper I wrote for <a href="https://www.mccormick.northwestern.edu/research-faculty/directory/profiles/forbus-ken.html">Ken Forbus</a>&rsquo; Qualitative Reasoning course, adapted for this blog. You can find a printable version of the paper <a href="/files/anthropomorphic-chess-evaluation-via-qualitative-analysis.pdf">here</a> and part 2 <a href="/posts/qualitative-analysis-chess/">here</a>.</strong></em></p>
<p>Computers that play chess, otherwise known as chess engines, have existed <a href="https://www.youtube.com/watch?v=wrxdWkjmhKg">since at least the late 1940s</a>. Because the game was said to require the perfect combination of planning, strategy, psychology, and calculation, chess was once thought to be an activity directly correlated with intelligence, and that only a truly intelligent computer should be able to defeat humans. However, as a recent chess.com <a href="https://drive.google.com/file/d/11IokKgTVSXdpYEzAuyViIleSZ_2wl0ag/view">report</a> explains, computers are now far stronger than humans:</p>
<blockquote>
<p>Human chess and computer chess are different, even at the highest levels. The best humans play at an Elo rating of 2800. “Stockfish,” the most powerful chess engine, has an estimated rating of more than 3500. In a theoretical match between World Champion Magnus Carlsen vs. Stockfish, we estimate that it is most likely that Magnus Carlsen would lose every single game—no wins and no draws.</p>
</blockquote>
<p>The supremacy of Machine over Man should be marked as a great triumph in artificial intelligence, and in a broad sense it is. However, I would argue that playing skill should not be the only goal, and a truly useful engine should be interpretable as well. We&rsquo;ve already &ldquo;solved&rdquo; the problem of playing chess at an extremely high level. Yet for historical reasons, interpretability was sacrificed for the sake of speed and playing strength. However, these tradeoffs may not make as much sense today. <a href="/posts/qualitative-analysis-chess/">Part 2</a> discusses opportunities and clear benefits for reevaluating those decisions moving forward.</p>
<h1 id="background">Background</h1>
<p>The general algorithm for performing search in zero-sum perfect information games like chess is known as minimax. The algorithm attempts to find the move which minimizes the opponent&rsquo;s maximum (i.e. best) move. Minimax visualizes the possible future state of the game as a tree, and the value of each node in the tree as a function of the nodes following from it. If you could compute the full game tree (in simple games like tik-tac-toe this is possible), you would be able to enumerate all possible terminal nodes, and propagate the result (1 for a win, -1 for a loss, 0 for a draw) back up the tree.</p>
<p>That&rsquo;s not possible in chess. The game tree is too big. Instead, the value of many of the nodes needs to be <em>estimated using heuristics</em> so that they can be propagated upwards. Therefore a fast and accurate static evaluation function is critical. (However, since the problem is recursive, the problem of creating a perfectly accurate static evaluator is as hard as evaluating that node&rsquo;s entire associated game tree.)</p>
<p>Minimax is often augmented with “alpha-beta pruning” to reduce the number of positions which will be evaluated. This effectively cuts the <a href="https://imgur.com/wRZINqS">computational complexity exponent in half</a> by removing from consideration those branches which cannot affect the final result.</p>
<p>Other challenges in minimax evaluation exist as well. In particular, a phenomenon dubbed the “Horizon Effect” is a peculiar failure mode of minimax searches. The Horizon Effect was <a href="https://drive.google.com/file/d/1XHhMQUgZHhD8Be7Klrl7_RMHQm8B9Zlc/view">first described</a> by grandmaster and computer scientist Hans Berliner in 1975. His illustration of the problem (his Figure 1.3) is reproduced in Figure 1. Algorithms that do not account for the Horizon Effect will try to “push” bad outcomes of their search beyond their search horizon, instead opting to make hopeless moves which only serve to delay the inevitable. As humans we intuitively understand this illusion.</p>
<figure><img src="/img/human_analysis_fig1.png"
			alt="Figure 1. White to move. Here, white’s bishop on a4 is doomed, attacked by black’s pawn on b5. White could delay the inevitable by moving his bishop to b3, but then black simply seals the bishop’s fate with pawn to c4. In that position, white does not have time to save his bishop, and it will be captured no matter what on the next move by the pawn on c4. Due to the Horizon Effect, at a limited depth white will not recognize this and will play hopeless moves like pawn from e4 to e5, temporarily attacking the knight but easily parried by capturing with the pawn on d6. This phenomenon is deemed the “Horizon Effect” because by pushing negative outcomes beyond the “horizon” of calculation depth, the engine is able to trick itself into believing that the problem doesn’t exist. A true case of “see no evil”."><figcaption>
			<p>Figure 1. White to move. Here, white’s bishop on a4 is doomed, attacked by black’s pawn on b5. White could delay the inevitable by moving his bishop to b3, but then black simply seals the bishop’s fate with pawn to c4. In that position, white does not have time to save his bishop, and it will be captured no matter what on the next move by the pawn on c4. Due to the Horizon Effect, at a limited depth white will not recognize this and will play hopeless moves like pawn from e4 to e5, temporarily attacking the knight but easily parried by capturing with the pawn on d6. This phenomenon is deemed the “Horizon Effect” because by pushing negative outcomes beyond the “horizon” of calculation depth, the engine is able to trick itself into believing that the problem doesn’t exist. A true case of “see no evil”.</p>
		</figcaption>
</figure>

<h2 id="branching-factor">Branching Factor</h2>
<p>In any given position, there may be many possible moves. By some estimates, chess has an average branching factor of around 30, with other estimates putting the number around 40. It isn’t easy to find a concrete value for the branching factor of chess. One source claims (without citation) that the branching factor may be <a href="https://courses.csail.mit.edu/6.034f/ai3/rest.pdf">up to 40</a> (see pg. 117). However, a more recent statistical <a href="https://chess.stackexchange.com/a/24325">analysis</a> of 2.5 million chess games put the real number closer to 31.</p>
<figure><img src="/img/avg_moves_available.png"
			alt="The branching factor depends on the stage of the game; middle games have far more available moves than end games. Because not all games are the same length, shorter games will tend to have higher average branching factors than longer ones. Barnes&rsquo; graphic is reproduced above."><figcaption>
			<p>The branching factor depends on the stage of the game; middle games have far more available moves than end games. Because not all games are the same length, shorter games will tend to have higher average branching factors than longer ones. Barnes&rsquo; graphic is reproduced above.</p>
		</figcaption>
</figure>

<p>In any case, it is possible to dramatically reduce the branching factor by employing “selective search”, which means excluding nodes from recursive tree search altogether. Reducing the number of possible moves that will be explored in any given position promises dramatic computational speedups, allowing the tree to be searched deeper at the expense of width. Given the slow speeds (by contemporary standards) of early computers, the allure of such a technique should be clear. It is also a more psychologically plausible way of playing, since most players quickly rule out ridiculous seeming moves and focus on the most promising ones.</p>
<h1 id="early-chess-engines">Early Chess Engines</h1>
<p>An early attempt to narrow the search tree was proposed by Berliner, who in 1975 devised CAPS-II which utilized a tree search algorithm with five total “reference levels” including ALPHA and BETA. His paper also was cognizant of the work of Chase and Simon from two years prior, recognizing the need for a bottom-up board representation. Unfortunately, his board representation was largely geometrical and included little in the way of qualitative relationships between pieces. Nevertheless, the resulting program was able to solve tactical puzzles in a quite impressive manner for the time.</p>
<p>Another example of the use of such a narrowed search tree is <a href="https://drive.google.com/file/d/1I54sVUe4Ybkln5hy5_MlZBnmrZBqbj2l/view?usp=sharing">PARADISE (PAttern Recognition Applied to DIrecting SEarch)</a>. This system used a knowledge base containing 200 “production rules” to match board patterns and determine the best move at any time. An example production rule is shown in Figure 2.</p>
<figure><img src="/img/human_analysis_fig2.png"
			alt="Figure 2. A sample production rule from the PARADISE knowledge base. This production rule detects and acts upon opponent’s trapped pieces. A trapped piece is identified as a non-pawn defensive piece which cannot move to another square without being captured and is not already attacked. Finally, the production rule describes the threat of this action (winning the piece) and how likely it is to succeed."><figcaption>
			<p>Figure 2. A sample production rule from the PARADISE knowledge base. This production rule detects and acts upon opponent’s trapped pieces. A trapped piece is identified as a non-pawn defensive piece which cannot move to another square without being captured and is not already attacked. Finally, the production rule describes the threat of this action (winning the piece) and how likely it is to succeed.</p>
		</figcaption>
</figure>

<p>Because the tree search was narrower, the system was able to search to higher depths. Still, because of hardware limitations of the time, PARADISE was extremely slow, generating only 109 nodes in 20 minutes of search time. (Current chess engines evaluate millions of nodes per second.)</p>
<p>PARADISE executes static analysis on a position using its many production rules with the ultimate goal of creating and executing a plan. Matched production rules post concepts to the database in addition to information about the intentions of the concept and its likelihood of success.</p>
<p>The program is then able to use this information to form a plan, which is a set of actions for the side to move, along with corresponding plans for each defensive alternative. Because there may be many potential alternatives at each move, this plan includes many branches.</p>
<figure><img src="/img/human_analysis_fig3.png"
			alt="Figure 3. White to move, PARADISE has produced a plan which involves checking the black king by moving the knight to g5, then checking the black king by moving the rook to d7. Depending on black’s next move white will then try to either capture the queen on d4 or the rook on d7."><figcaption>
			<p>Figure 3. White to move, PARADISE has produced a plan which involves checking the black king by moving the knight to g5, then checking the black king by moving the rook to d7. Depending on black’s next move white will then try to either capture the queen on d4 or the rook on d7.</p>
		</figcaption>
</figure>

<p>An example of such a plan is reproduced in Figure 3 above. (An interesting sidebar about this position is that based upon Stockfish analysis, this position results in inescapable checkmate for black within 15 moves. That is, even with optimal play, black will be checkmated in 15 or fewer moves. The winning sequence begins with white moving his queen to e5, exploiting the pin on the f6 pawn from the f1 rook.)</p>
<p>Searching only selective lines is more difficult to implement than full-width search. Further, selective searches may miss important continuations of a position, causing the computer to select an incorrect move. It was for this reason that Berliner himself, an original proponent for the application of strict logical rules in chess, decided to seek <a href="https://www.nytimes.com/2017/01/16/business/hans-berliner-master-chess-player-and-programmer-dies-at-87.html">brute-force search methods</a> instead.</p>
<h1 id="modern-chess-engines">Modern Chess Engines</h1>
<p>Stockfish is currently the #1 ranked chess engine in the world. Stockfish is open-source and performs a “full width” search on the game tree. Leaf nodes are evaluated using either a classical, hand-crafted evaluation heuristic, or more recently, a neural network evaluator called NNUE. The classical evaluation function uses a set of around 30 factors weighted empirically using a dedicated testing framework called Fishtest. Altogether, the project has <a href="https://github.com/official-stockfish/Stockfish">around 200 contributors</a>. Optimized for speed, Stockfish can evaluate around <a href="https://chessify.me/blog/nps-what-are-the-nodes-per-second-in-chess-engine-analysis">millions of nodes per second</a> on a typical 4-core computer.</p>
<figure><img src="/img/stockfish_starting_pos.png"
			alt="Stockfish analyzing the starting position on my laptop at around 1 million nodes per second."><figcaption>
			<p>Stockfish analyzing the starting position on my laptop at around 1 million nodes per second.</p>
		</figcaption>
</figure>

<p>Neural networks may also be used more directly. <a href="https://lczero.org/dev/wiki/technical-explanation-of-leela-chess-zero/">Leela Chess Zero</a>, also known as Lc0 is another open-source chess engine, modeled after DeepMind’s AlphaZero chess engine. Lc0 uses Predictor + Upper Confidence Bound tree search (PUCT) to search its game tree. Leela evaluates new nodes by iteratively choosing moves from a probability distribution until it reaches an unexplored node. At that point, Leela&rsquo;s neural network estimates the node&rsquo;s value and propagates that value back up the tree. PUCT is very similar to Monte Carlo Tree Search, but with game “rollouts” replaced by neural network evaluations.</p>
<p>Another notable engine is <a href="https://arxiv.org/abs/2006.01855">Maia Chess</a>. Maia is interesting for two reasons. Unlike most other engines, Maia&rsquo;s objective is to model human behavior rather than to perform optimally. This is interesting because Maia is in many cases trained on suboptimal data. The other notable feature about Maia is the unusual manner in which moves are selected: instead of performing any type of tree search at all, the engine simply returns the neural network&rsquo;s static evaluation of the current board position.</p>
<p>We can also attempt to predict the evaluation of Stockfish directly with a neural network, as <a href="https://arxiv.org/pdf/2402.04494v1">Deepmind did in 2024</a> and as I did using <a href="/posts/distilling-stockfish/">one billion positions from the Gigafish dataset</a>.</p>
<p>Suffice it to say, however, that despite computation no longer being a significant limitation, prior qualitative approaches still have garnered relatively little attention.</p>
<h1 id="conclusion">Conclusion</h1>
<p>Engineers have historically sought to build the most powerful chess engine possible. With the singular goal of defeating human (and now computer) opponents, explainability was an expedient but understandable tradeoff to make. In the present day, however, there is no longer a question of whether humans or computers are superior chess players. It seems quite clear that the time has come to revisit some of the tradeoffs made in the past.</p>
<p>In <a href="/posts/qualitative-analysis-chess/">part 2</a> I will discuss some clear benefits of a more interpretable chess engine, and some possible routes of getting there.</p>
]]></content>
        </item>
        
        <item>
            <title>Opponent Modeling Wins 2× Faster Than Stockfish</title>
            <link>https://blog.lukesalamone.com/posts/winning-faster-than-stockfish/</link>
            <pubDate>Sat, 02 Jul 2022 16:24:10 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/winning-faster-than-stockfish/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;p&gt;&lt;strong&gt;TLDR: Using opponent modeling we can win 2x faster than Stockfish by playing high risk, high reward moves that Stockfish will avoid.&lt;/strong&gt;&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<p><strong>TLDR: Using opponent modeling we can win 2x faster than Stockfish by playing high risk, high reward moves that Stockfish will avoid.</strong></p>
<p>Minimax solves for optimal opponent play, minimizing the best move an opponent could make. But what if we knew the opponent wouldn&rsquo;t make the best move? What if we knew what the opponent would do ahead of time? In that case, we could beat them faster by playing moves which take advantage of this fact, even if our move isn&rsquo;t objectively the best move. <a href="https://www.youtube.com/watch?v=XsdefZRavj4">Don&rsquo;t play the game, play the man.</a></p>
<p>For the purposes of this problem let&rsquo;s assume it is a chess algorithm but this concept can be generalized to any algorithm which can use minimax. Anyone who has played a game against another person understands the idea of &ldquo;downloading&rdquo; your opponent: playing against a person enough that you feel like you know what they will do. <a href="https://www.youtube.com/watch?v=42LatV92k1s">Hikaru Nakamura does</a>.</p>
<p>It seems like this type of player would have many possible applications, including training for specific skill levels and even trainers personalized to an individual&rsquo;s playing style.</p>
<h2 id="prior-work">Prior work</h2>
<p>There has been <a href="https://www.chessprogramming.org/Opponent_Model_Search#Further_Research">previous work</a> in terms of opponent modeling in chess. <a href="https://maiachess.com/">Maia chess</a> has been trained to model several Elo levels of opponent play which makes it ideal to be used as a backbone in my experiments.</p>
<h2 id="the-policy-function">The Policy Function</h2>
<p>We can think of the policy function as a model of the opponent. We will therefore be optimizing an algorithm to play against this model, rather than the optimal opponent.</p>
<p>The specifics of the policy function are as follows. The function takes two parameters, a board position <code>B</code> and an opponent skill level <code>L</code>. The parameter <code>L</code> is important because the <em>nature</em> of the opponent&rsquo;s suboptimal play is important. All levels of chess players play sub-optimally, but the types of mistakes a beginner or intermediate-level player will make are different from a grandmaster. The algorithm should exploit this fact. In practice, this algorithm should play in a much more &ldquo;trappy&rdquo; way than the methodical slow grind of a typical chess engine.</p>
<p>Formally, at move $i$ there will be $n_i$ possible moves. For each of the $n_i$ moves, we should calculate an evaluation weighted by the opponent&rsquo;s probability of playing responses to that move:</p>
<p style="text-align: center; font-size: 1.5em">
$ e_i(m_i) = p_L(M_{i+1}) \cdot r(M_{i+1}) $
</p>
<p>Where $M_{i+1}$ is the set of possible moves the opponent can make in response to move $m_i$, $p_L$ is the probability distribution of each move in $M_i$ being made, and $r$ is some real evaluation of the board position.</p>
<p>Let&rsquo;s use an example. Suppose that at some stage in the game the algorithm must choose between 2 possible moves. For simplicity, the opponent will have 4 possible replies after each of the two possible moves. After the algorithm makes the the first possible move, the probabilities look like this:</p>
<h3 id="opponent-move-distribution-at-position-1">Opponent move distribution at position 1</h3>
<table>
	<thead>
			<tr>
					<th>Opponent Move</th>
					<th>Probability of play</th>
					<th>Resulting position evaluation</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>0.6</td>
					<td>5</td>
			</tr>
			<tr>
					<td>2</td>
					<td>0.3</td>
					<td>1</td>
			</tr>
			<tr>
					<td>3</td>
					<td>0.1</td>
					<td>0.5</td>
			</tr>
			<tr>
					<td>4</td>
					<td>0.1</td>
					<td>-1</td>
			</tr>
	</tbody>
</table>
<p style="text-align: center; font-size: 1.5em">
$ \begin{bmatrix} 0.6 \\ 0.3 \\ 0.1 \\ 0.1 \end{bmatrix}
\cdot \begin{bmatrix} 5 \\ 1 \\ 0.5 \\ -1 \end{bmatrix} = 3.25 $
</p>
<h3 id="opponent-move-distribution-at-position-2">Opponent move distribution at position 2</h3>
<p>And after the second possible move, the opponent move distribution looks like this:</p>
<table>
	<thead>
			<tr>
					<th>Opponent Move</th>
					<th>Probability of play</th>
					<th>Resulting position evaluation</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>0.9</td>
					<td>4</td>
			</tr>
			<tr>
					<td>2</td>
					<td>0.05</td>
					<td>0.5</td>
			</tr>
			<tr>
					<td>3</td>
					<td>0.03</td>
					<td>0</td>
			</tr>
			<tr>
					<td>4</td>
					<td>0.02</td>
					<td>-2</td>
			</tr>
	</tbody>
</table>
<p style="text-align: center; font-size: 1.5em">
$ \begin{bmatrix} 0.9 \\ 0.05 \\ 0.03 \\ 0.02 \end{bmatrix}
\cdot \begin{bmatrix} 4 \\ 0.5 \\ 0 \\ -2 \end{bmatrix} = 3.585 $
</p>
<p><em>In these tables positions are evaluated from the algorithm&rsquo;s point of view, so higher evaluation=better. Assume the evaluation function is a standard evaluation from a chess engine e.g. Stockfish.</em></p>
<p>In this example it is clear that the algorithm should choose move 2, because it is highly likely that after making this move the algorithm will have a decisive advantage.</p>
<h2 id="checkmates">Checkmates</h2>
<p>All of this works quite well in the middle-game, where position evaluations are all within a reasonable range. But what happens when one of the opponent&rsquo;s moves leads to checkmate? How much value should we give to the possibility that an opponent might blunder into checkmate? The above calculation only works for finite numbers. The evaluation of a checkmate is +/- infinity. Instead, let&rsquo;s choose a large, finite number for checkmate.</p>
<p>The behavior of the algorithm depends heavily on our choice for this checkmate evaluation, the <code>CHECKMATE_WEIGHT</code>. If the number is too high, the algorithm will favor positions where the opponent <em>might</em> get checkmated, playing risky moves. If the number is too low, the algorithm will choose other branches which win less quickly. After a bit of trial and error, I settled on +10 for checkmate. This is enough for the algorithm to seek out these types of positions.</p>
<p>A further question is what to do once checkmate is inevitable. That is, what should the algorithm do once it will definitely win? One option is to continue to sample from the distribution, continuing to play the move which maximizes winning chances. At the beginning, this was my intuition. Unfortunately, this leads to many draws by repetition.</p>
<p>Instead, once the game is in a forced-win situation (&ldquo;mate in N moves&rdquo;) the algorithm will stop sampling from the distribution and simply play the moves which lead to checkmate. While this will inevitably lead to a win, it changes the character of the algorithm somewhat. Therefore, I have limited the lookahead to checkmates in the immediate next few moves (specifically, mate in 5 or fewer moves). Since the original goal was to create an algorithm which is used for training, this seems reasonable.</p>
<h2 id="games">Games</h2>
<table>
	<thead>
			<tr>
					<th>White</th>
					<th>Black</th>
					<th>Games</th>
					<th>White win %</th>
					<th>Average half moves per game</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Antimaia 1900</td>
					<td>Maia 1900</td>
					<td>100</td>
					<td>100%</td>
					<td>37</td>
			</tr>
			<tr>
					<td>Maia 1900</td>
					<td>Antimaia 1900</td>
					<td>100</td>
					<td>0%</td>
					<td>43</td>
			</tr>
			<tr>
					<td>Maia 1900</td>
					<td>Stockfish @10</td>
					<td>100</td>
					<td>0%</td>
					<td>67</td>
			</tr>
			<tr>
					<td>Stockfish @10</td>
					<td>Maia 1900</td>
					<td>100</td>
					<td>100%</td>
					<td>70</td>
			</tr>
	</tbody>
</table>
<p>I simulated 400 matches testing this opponent modeling approach, which I wrote about <a href="/posts/best-antimaia-games/">here</a>.</p>
<p>Key findings:</p>
<ul>
<li>Both Antimaia and Stockfish@10 beat Maia 100% of the time</li>
<li>Antimaia finishes its games much more quickly (12-16 moves faster!)</li>
<li>Antimaia is exploiting weaknesses in Maia&rsquo;s policy by playing high risk/high reward moves</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Machine Learning Glossary</title>
            <link>https://blog.lukesalamone.com/posts/ml-glossary/</link>
            <pubDate>Sun, 26 Jun 2022 22:45:18 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/ml-glossary/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;h2 id=&#34;batch-norm&#34;&gt;Batch Norm&lt;/h2&gt;
&lt;p&gt;Normalizes the mean and variance for all instances of a feature in a batch. Commonly used in older architectures like CNNs by smoothing out the loss landscape and allowing for higher learning rates and faster convergence. Example implementation:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<h2 id="batch-norm">Batch Norm</h2>
<p>Normalizes the mean and variance for all instances of a feature in a batch. Commonly used in older architectures like CNNs by smoothing out the loss landscape and allowing for higher learning rates and faster convergence. Example implementation:</p>
<pre><code class="language-python">def batch_norm(features: np.array, i: int, eps=1e-5) -&gt; np.array:
    x = features[:,i]
    mean = x.mean()
    variance = x.var()
    x = (x - mean) / np.sqrt(variance + eps)
    return x
</code></pre>
<p>In practice, we include a gamma and beta parameter which are learnable parameters of length F, where F is the number of features. These parameters that allow the network to undo the normalization if needed.</p>
<pre><code class="language-python">def batch_norm2(
        features: np.array,
        gamma: np.array,
        beta: np.array,
        eps=1e-5
    ):
        mean = features.mean(axis=0)
        variance = features.var(axis=0)
        normalized = (features - mean) / np.sqrt(variance + eps)
        return gamma * normalized + beta
</code></pre>
<p>See also: <a href="/posts/ml-glossary/#layer-norm">Layer norm</a></p>
<h2 id="byte-pair-encoding-bpe">Byte Pair Encoding (BPE)</h2>
<p>A tokenization algorithm which iteratively merges the most frequent pair of tokens. For example, let&rsquo;s take the sentence <code>snug_as_a_bug_in_a_rug</code> as our corpus (underscores used to make things easier to see).</p>
<p>First, we break up the text into its individual characters:</p>
<pre><code class="language-python">corpus: ['s', 'n', 'u', 'g', '_', 'a', 's', '_', 'a', '_', 'b', 'u', 'g', '_', 'i', 'n', '_', 'a', '_', 'r', 'u', 'g']
vocab: ['s', 'n', 'u', 'g', '_', 'a', 'b', 'i', 'r']
</code></pre>
<p>After splitting by chars, we have 9 unique tokens in our vocabulary. Next, we merge the most common pair in our vocabulary, <code>('u','g') =&gt; 'ug'</code> and retokenize:</p>
<pre><code class="language-python">corpus: ['s', 'n', 'ug', '_', 'a', 's', '_', 'a', '_', 'b', 'ug', '_', 'i', 'n', '_', 'a', '_', 'r', 'ug']
vocab: ['s', 'n', 'u', 'g', '_', 'a', 'b', 'i', 'r', 'ug']
</code></pre>
<p>Then, we find that the most common pair is <code>('_','a')</code> which gives us the next merged token <code>&quot;_a&quot;</code>:</p>
<pre><code class="language-python">corpus: ['s', 'n', 'ug', '_a', 's', '_a', '_', 'b', 'ug', '_', 'i', 'n', '_a', '_', 'r', 'ug']
vocab: ['s', 'n', 'u', 'g', '_', 'a', 'b', 'i', 'r', 'ug', '_a']
</code></pre>
<p>We can repeat this process until the vocabulary is the size we need.</p>
<p>See also: <a href="/posts/ml-glossary/#wordpiece-tokenization">Wordpiece tokenization</a></p>
<h2 id="direct-preference-optimization">Direct Preference Optimization</h2>
<p><a href="https://arxiv.org/pdf/2305.18290">Direct preference optimization</a> (DPO) is a contrastive learning method, often used for fine-tuning an LLM to align with human preferences.</p>
<p>Like other contrastive learning methods (for example those used to fine-tune encoders) DPO works with <code>(anchor, positive, negative)</code> triplets. In this case, the anchor is the prompt, the positive sample is the preferred completion, and the negative sample is the bad completion. By computing the probabilities the models assign to each of the responses, we can train the model to assign higher probabilities to the preferred response.</p>
<p>DPO is simpler, more stable, and more lightweight than more traditional methods like PPO and RLHF. It does not require a second model at training time like PPO and RLHF do. It doesn&rsquo;t require a separately trained reward model at all. The model is updated by simple gradient descent rather than generating during training.</p>
<p>However, since DPO is technically off-policy from an RL perspective, since the samples it is scoring are not actually generated from the model.</p>
<p>DPO pairs technically do not need to be created based on human preferences. Any reward model will do. For example:</p>
<ul>
<li>If we&rsquo;re training a chess model, we can use a chess engine to score potential moves</li>
<li>If we&rsquo;re training a code generation model, we can use the compiler&rsquo;s feedback to create positive and negative samples</li>
</ul>
<h2 id="distillation">Distillation</h2>
<p>Distillation is the task of training a smaller &ldquo;student&rdquo; model to behave like a larger &ldquo;teacher&rdquo; model.</p>
<p>Classically, this has meant directly using the teacher&rsquo;s output distribution, and minimizing <code>KL(p_teacher || p_student)</code>.</p>
<p>Less formally, the larger teacher model can be used as a pseudo-labeler of real inputs, and a smaller student can learn from those labels.</p>
<h2 id="entropy">Entropy</h2>
<p>When training LLMs, we want some level of the model&rsquo;s uncertainty when generating tokens. To do this, we multiply the probability by the negative log of the probability.</p>
<pre><code class="language-python">import math
def calc_per_token_entropy(probabilities):
    entropy = 0
    for probability in probabilities:
        entropy -= probability * math.log(probability)
    return entropy
</code></pre>
<p>The negative log of the probability can be thought of as the &ldquo;surprise&rdquo; from that token: high probability tokens have very little surprise to them, and low probability tokens have surprise approaching infinity. Then, multiplying by the probability itself helps to balance the total by preventing it from exploding.</p>
<p>There is a connection to <a href="/posts/perplexity/">perplexity</a>, which is $ e^{H(x)} $, where $H(x)$ is the entropy of the distribution.</p>
<h2 id="group-relative-preference-optimization-grpo">Group-Relative Preference Optimization (GRPO)</h2>
<p>GRPO is an RL algorithm for LLM post-training which attempts to determine how good a rollout is in order to apply a reward to it.</p>
<p>GRPO has some advantages over more traditional PPO. In PPO, we use a second model (the &ldquo;critic&rdquo;) to score rollouts, and in practice this model may on the order of the same size as the model we&rsquo;re training. This is both slow and resource intensive.</p>
<p>In GRPO, we get rid of the critic entirely.</p>
<pre><code class="language-python">prompt = 'write a python function to determine if a number is prime'
group_size = 8 # usually between 4 and 16
responses = [
    generate_response(model, prompt, temp=1.1)
    for _ in range(group_size)
]

rewards = [
    calculate_reward(response) for response in responses
]

mean_reward = np.mean(rewards)
std_reward = np.std(rewards)
advantages = [(reward-mean_reward)/std_reward for reward in rewards]

# policy gradient update
log_probs = [model.compute_log_prob(r) for r in responses]
pg_loss = 0
for log_prob, advantage in zip(log_probs, advantages):
    pg_loss += -log_prob * advantage

# KL penalty prevents model from changing too quickly
ref_log_probs = [ref_model.compute_log_prob(r) for r in responses]
kl = sum(r - p for p, r in zip(log_probs, ref_log_probs)) / group_size

loss = pg_loss / group_size + beta * kl
</code></pre>
<h2 id="kl-divergence">KL-Divergence</h2>
<p>A measurement of the &ldquo;distance&rdquo; between two distributions.</p>
<pre><code class="language-python">from math import log

def kl_divergence(p:list[float], q:list[float]) -&gt; float:
  total = 0
  for a,b in zip(p, q):
    total += a * log(a/b) if p != 0 else 0
  return total
</code></pre>
<p>Note that KL-divergence is not symmetrical.</p>
<p>Where it shows up:</p>
<ul>
<li>In RLHF, it is used to measure the change between old policy and the updated policy, and prevent it from drifting too far using a KL penalty.</li>
<li>In knowledge distillation, we directly train on minimizing <code>KL(p_teacher||p_student)</code> on the distribution of logits.</li>
</ul>
<h2 id="layer-norm">Layer Norm</h2>
<p>This could probably be called &ldquo;token norm&rdquo; and avoid a lot of confusion since in practice this is often used in LLMs.</p>
<pre><code class="language-python">
# hypothetical input embeddings for &quot;ai is cool&quot;
sequence = np.array([
    [0.3, 0.1, 0.4, 0.1], # &quot;ai&quot;
    [0.5, 0.9, 0.2, 0.6], # &quot;is&quot;
    [0.5, 0.3, 0.5, 0.8]  # &quot;fun&quot;
])

def layer_norm(sequence, eps=1e-5):
    &quot;&quot;&quot;
    Example for learning, not vectorized.

    Here, `sequence` has dimensions (seq_length, emb_dim)
    and we are ignoring the batch dimension.
    &quot;&quot;&quot;
    means = []
    variances = []
    for emb in sequence:
        means.append(emb.mean())
        variances.append(emb.var())
    result = []
    for emb,mean,variance in zip(sequence, means, variances):
        emb -= mean
        emb /= np.sqrt(variance + eps)
        result.append(emb)
    return np.array(result)
</code></pre>
<p>Today, <a href="https://sebastianraschka.com/faq/docs/rmsnorm-vs-layernorm.html">many more modern LLMs use RMSNorm instead</a>, which skips subtracting the mean.</p>
<p>See also: <a href="/posts/ml-glossary/#batch-norm">Batch norm</a></p>
<h2 id="offline-rl">Offline RL</h2>
<p>Models are trained from a fixed dataset of preferences rather than the rollouts they would naturally generate. This is much faster, but can be less useful if the training data distribution shifts far from the the model&rsquo;s actual predictions.</p>
<p>Examples of offline RL algorithms include DPO, IPO, and KTO.</p>
<h2 id="online-rl">Online RL</h2>
<p>Models generate rollouts, get scored, and update from that signal. Considered stronger than offline RL because the model is getting direct feedback on its own predictions. However, the scoring itself can be expensive, and gradient updates must wait for the scores to come back.</p>
<p>Examples of online RL algorithms include PPO, GRPO, and REINFORCE.</p>
<h2 id="parallelism">Parallelism</h2>
<h3 id="data-parallel">Data parallel</h3>
<p>Replicate the same model over all devices.</p>
<ol>
<li>For each batch, split the data between devices.</li>
<li>Each copy computes a forward pass, a loss, and a backwards pass to compute local gradients.</li>
<li>Each device reports its gradients.</li>
<li>Each copy is updated with the same average gradient.</li>
</ol>
<h3 id="model-parallel">Model parallel</h3>
<p>This is used when a model is too large to fit on a single device.</p>
<p>The model can be split between multiple devices, with each device computing its part, handing it off to the next device, then waiting for the next batch.</p>
<p>This is very slow. Pipeline parallelism is an improvement to Model Parallelism.</p>
<h3 id="pipeline-parallel">Pipeline parallel</h3>
<p>The idea here is to reduce wait time between devices.</p>
<p>Rather than a single device computing and then waiting as in model parallelism, a device finishes its forward pass on a &ldquo;micro-batch&rdquo; and then immediately starts working on the next &ldquo;micro-batch&rdquo;.</p>
<p>Similarly, backwards passes flow in reverse.</p>
<h3 id="tensor-parallel">Tensor parallel</h3>
<p>This is used when even a single layer cannot fit on a device.</p>
<p>The individual tensors (weights and activations) can be split across multiple devices to enable a huge matrix multiplications.</p>
<h2 id="reinforcement-learning-from-human-feedback-rlhf">Reinforcement Learning from Human Feedback (RLHF)</h2>
<p>RLHF has three steps:</p>
<ol>
<li>Train a reward model on human preference pairs. This is very similar to a standard pairwise Learn-to-Rank model where we train on <code>(input, positive, negative)</code> triplets. You can simply add a linear layer to the top of the LLM to create <code>R(x,y)</code> which generates the ranking scores.</li>
</ol>
<p>2a. Calculate the expected reward $ E[R(x,y)] $</p>
<p>2b. Penalize large KL-divergence with the old policy</p>
<h2 id="rejection-samplingrejection-fine-tuning">Rejection Sampling/Rejection Fine-Tuning</h2>
<p>The idea of rejection sampling is to generate multiple responses or &ldquo;roll-outs&rdquo; to an input. Typically we will increase sampling temperature  Responses which do not meet some criterion are &ldquo;rejected&rdquo; or thrown away. This leaves the responses which do pass the criterion. If we fine-tune on these examples, this is called rejection fine-tuning.</p>
<p>RFT has some similarity with RL techniques like GRPO, which also involve using multiple roll-outs.</p>
<p>On the one hand, RFT is very simple and intuitive. It avoids a lot of the RL complexity such as entropy collapse and policy divergence. Additionally, because GRPO relies on the relative advantages within the group, RFT can outperform GRPO in situations where all members of the group have perfect or close to perfect rewards.</p>
<ul>
<li>If all members have perfect scores, the advantage will be zero for each completion.</li>
<li>If all members have close to perfect scores, GRPO will amplify noise within the group.</li>
</ul>
<p>On the other hand, RFT can be far less sample efficient than RL, because it only learns from positive examples (since the negatives are rejected). For hard problems, your acceptance rate for the problem may be very low.</p>
<ul>
<li>If your acceptance rate is 1 in 16, you will throw away 94% of your work to get the 6% to learn from.</li>
<li>Meanwhile with GRPO, if 1 out of 16 samples in the group is correct, your algorithm can learn from all 16.</li>
<li>However, if none of the samples in the group get a reward, the relative advantage for all members collapses to zero and you get nothing. In this case, it would be better to keep generating until we get a correct completion.</li>
</ul>
<p>For this reason, we can use RFT until the acceptance rate is high enough for GRPO.</p>
<h2 id="speculative-decoding">Speculative Decoding</h2>
<p>The idea of speculative decoding is to use a small &ldquo;draft&rdquo; model to speed up inference for the big &ldquo;target&rdquo; model. This is possible because the target model can predict probabilities for multiple sequences in parallel. For example, a small draft model might predict:</p>
<pre><code class="language-python">draft_tokens = ['take', 'me', 'out', 'to', 'the', 'ballgame']
</code></pre>
<p>Now, the target model can simply predict the probabilities for each of these in parallel:</p>
<pre><code class="language-python">probabilities = model([
  {'token': 'take'},
  {'token': 'me', 'prefix':['take']},
  {'token': 'out', 'prefix':['take', 'me']},
  {'token': 'to', 'prefix':['take', 'me', 'out']},
  {'token': 'the', 'prefix':['take', 'me', 'out', 'to']},
  {'token': 'ballgame', 'prefix':['take', 'me', 'out', 'to', 'the']},
])
</code></pre>
<p>Because we&rsquo;re not just using greedy decoding, we need to accept tokens from the draft model probabilistically. In other words, we accept a token with probability <code>p_accept(token) = min(1, target_prob(token) / draft_prob(token))</code>.</p>
<ul>
<li>So if the target model thinks the token is a lot less likely than the draft model does, the ratio will be small and <code>p_accept</code> will be small for that token.</li>
<li>On the other hand, if the target model agrees with the draft model the ratio will be close to 1 and <code>p_accept</code> will be close to 1.</li>
<li>In the case where the target model thinks the token is more likely than the draft model did, it&rsquo;s somewhat of a happy accident that the token was actually selected by the draft model but the acceptance probability will be 1.</li>
</ul>
<p>If the target model rejects a token that the draft model proposes, none of its subsequent tokens will be considered. The draft model will rerun with all of the accepted tokens and propose new tokens.</p>
<p>This is how it is possible to speed up the target model&rsquo;s inference without losing quality.</p>
<h2 id="wordpiece-tokenization">Wordpiece Tokenization</h2>
<p>An algorithm for training a tokenizer with a slightly more advanced rule for merging tokens. For example, consider the following pair frequencies in a corpus <code>abracadabra</code>:</p>
<pre><code class="language-python">Counter({
    ('a', 'b'): 4,
    ('b', 'a'): 3,
    ('a', 'c'): 2,
    ('d', 'a'): 2,
    ('c', 'a'): 1,
    ('c', 'd'): 1,
    ('a', 'd'): 1,
})
</code></pre>
<p>Under byte-pair encoding tokenization, we would automatically merge <code>('a','b') =&gt; 'ab'</code>. However, wordpiece tokenization also takes the frequencies of the tokens into account individually as well:</p>
<pre><code class="language-python">Counter({
    'a':7,
    'b':4,
    'c':2,
    'd':2
})
</code></pre>
<p>In wordpiece tokenization, we merge the tokens when the pair maximizes <code>score(x,y) = freq(xy) / (freq(x) * freq(y))</code>. In other words, we penalize pairs if they are made up of common tokens. Let&rsquo;s compute the scores:</p>
<pre><code class="language-python">scores = {
    ('a', 'b'): 4÷(7*4) = 0.14,
    ('b', 'a'): 3÷(4*7) = 0.11,
    ('a', 'c'): 2÷(7*2) = 0.14,
    ('d', 'a'): 2÷(2*7) = 0.14,
    ('c', 'a'): 1÷(2*7) = 0.04,
    ('c', 'd'): 1÷(2*2) = 0.25,
    ('a', 'd'): 1÷(7*2) = 0.04,
}
</code></pre>
<p>In this case, <code>('c','d')</code> is actually the winning pair because it is composed of rare tokens.</p>
]]></content>
        </item>
        
        <item>
            <title>Alphabet Chess</title>
            <link>https://blog.lukesalamone.com/posts/alphabet-chess/</link>
            <pubDate>Fri, 10 Jun 2022 23:56:14 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/alphabet-chess/</guid>
            <description>&lt;p&gt;&lt;em&gt;&lt;strong&gt;TLDR: Alphabet chess is a chess variant that allows handicapping by mixing in a bit of poker into the beginning of the game. Moves must be played according to a secret word at the beginning of the game.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Chess has been played in different forms since the seventh century, and in its modern form since the nineteenth century. Opening theory, i.e. the study of the best moves to begin the game with, has been developing since then.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><em><strong>TLDR: Alphabet chess is a chess variant that allows handicapping by mixing in a bit of poker into the beginning of the game. Moves must be played according to a secret word at the beginning of the game.</strong></em></p>
<p>Chess has been played in different forms since the seventh century, and in its modern form since the nineteenth century. Opening theory, i.e. the study of the best moves to begin the game with, has been developing since then.</p>
<h2 id="the-egg-opening">The EGG Opening</h2>
<p>I was inspired the other day after watching a <a href="https://www.youtube.com/watch?v=J6G3cP991Yc">video</a> by Eric Rosen called &ldquo;Quadruple Egg&rdquo;. Chess boards are usually notated from left to right with the letters A through H. (These columns are called &ldquo;files&rdquo;.) The &ldquo;egg&rdquo; opening, therefore, involves moving the E pawn, then the G pawn twice, spelling the word &ldquo;egg&rdquo;. It&rsquo;s an extremely unconventional opening, but how bad is it compared with unrestricted openings?</p>
<h2 id="what-are-the-best-ie-least-bad-alphabetic-openings">What are the best (i.e. least bad) alphabetic openings?</h2>
<p>We can call the Egg Opening one version of an alphabetical chess opening: an opening where the piece moved must start on a file corresponding to the next letter in a given word. In this definition, the pieces don&rsquo;t have to be pawns.</p>
<p>This raises an interesting question: what are the best and worst alphabetical openings? Under engine evaluation, they will all be losing, because the opponent is not limited in which piece they can move. However, as we will see, some are much worse than others.</p>
<h3 id="four-letters">Four letters</h3>
<p>English doesn&rsquo;t have an official dictionary, so I chose <a href="https://gist.github.com/paulcc/3799331">this list</a> of four letter words. From there, we need to eliminate all words with letters after H. This leaves us with <a href="https://gist.github.com/lukesalamone/eab96ddde6eb326b8e339c41f5f52bda">37 words</a>. Don&rsquo;t ask me what they all mean:</p>
<pre><code>abbe, abed, aced, ache, aged, agha,
baba, babe, bach, bade, bead, beef,
cafe, cage, ceca, cede, chad, chef,
dace, dada, dead, deaf, deed,
each, edda, edge, egad,
face, fade, feed,
gaff, gaga, gage, geed, ghee,
head, heed
</code></pre>
<p>A simple way of measuring how bad each of these is would be to evaluate them against Stockfish to see how badly we&rsquo;re losing after playing these moves. In this simulation, we play as white each time.</p>
<p><img src="/img/alpha_chess_disadvantage_depth_20.png" alt="depth 20"></p>
<p>This measurement approach has a problem, though: Stockfish is assuming that white can make any move, and playing black&rsquo;s moves accordingly. In reality, white&rsquo;s moves are extremely restricted. If black knew about this restriction, he would be much more aggressive.</p>
<p>Further, if black were to discover which word white is playing, he could punish white. And even if black doesn&rsquo;t know exactly which word white is playing, black could eliminate moves which definitely can&rsquo;t form English words. For example, if white&rsquo;s first move is H, the next move will definitely be E.</p>
<h2 id="alphabetical-chess">Alphabetical Chess</h2>
<p>What if both players were required to play their first four moves from dictionary words? And what if those words were pre-assigned but secret to each other? For example, if each player drew a card from a deck. In this game, if a player is unable to make a move with a piece of the correct letter, they lose.</p>
<p>There are a few interesting features of this chess variant:</p>
<ol>
<li>Your starting word will have a big impact on how the game will turn out.</li>
<li>Figuring out your opponent&rsquo;s word will give you a big advantage.</li>
<li>Checks and other attacks can be devastating. You may not be able to respond to the attack, and checks may end the game immediately.</li>
</ol>
<p>In that case, it isn&rsquo;t necessarily true that the best and worst words from above will still be the best and worst when pitted against another randomly chosen word. It&rsquo;s a rock-paper-scissors situation: depending on which word A you happen to get, there is another word B which best counters your word, and another word C which your word best counters.</p>
<p><img src="/img/alpha_chess_heatmap.png" alt="alphabet chess heatmap"></p>
<p>Here the y-axis shows the word which was played with the white pieces, and the x-axis shows the word played with black.</p>
<h2 id="cool-things-about-alphabet-chess">Cool Things About Alphabet Chess</h2>
<p>There are a few interesting benefits to this variant:</p>
<ol>
<li>
<p>Handicaps are quite easily built in. The longer your secret word is, the more restricted your play is and the more of a handicap you will have.</p>
</li>
<li>
<p>An element of gamesmanship and imperfect information. Players can win on the board with good moves, but also &ldquo;off-board&rdquo; by figuring out their opponent&rsquo;s secret word.</p>
</li>
<li>
<p>Prevents good players from using memorized openings.</p>
</li>
</ol>
<h2 id="play-alphabet-chess">Play Alphabet Chess</h2>
<p>If you&rsquo;d like to try it out with a friend and a chessboard, here&rsquo;s an online tool that&rsquo;ll pick a random word: <a href="https://alphachess.surge.sh">https://alphachess.surge.sh</a></p>
]]></content>
        </item>
        
        <item>
            <title>Paper Summary: COMET (Knowledge Graph Construction)</title>
            <link>https://blog.lukesalamone.com/posts/knowledge-graph-construction/</link>
            <pubDate>Tue, 17 May 2022 17:47:25 +0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/knowledge-graph-construction/</guid>
            <description>&lt;figure&gt;&lt;img src=&#34;../img/comet_knowledge_generation.png&#34;
			alt=&#34;Selected {subject, relation, object} tuples generated by COMET&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;Selected {subject, relation, object} tuples generated by COMET&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Paper link: &lt;a href=&#34;https://arxiv.org/abs/1906.05317&#34;&gt;https://arxiv.org/abs/1906.05317&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This paper describes COMET, a method of generating knowledge bases automatically. Previous work largely focused on encyclopedic knowledge, which has well-defined relationships. This paper, however, focuses on commonsense knowledge. In this paper the authors introduce a “commonsense transformer” which trains on a knowledge base consisting of tuples and a pre-trained language model. Their trained model generates new nodes in the knowledge graph and completes phrases based on edges in the existing graph.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<figure><img src="/img/comet_knowledge_generation.png"
			alt="Selected {subject, relation, object} tuples generated by COMET"><figcaption>
			<p>Selected {subject, relation, object} tuples generated by COMET</p>
		</figcaption>
</figure>

<p>Paper link: <a href="https://arxiv.org/abs/1906.05317">https://arxiv.org/abs/1906.05317</a></p>
<p>This paper describes COMET, a method of generating knowledge bases automatically. Previous work largely focused on encyclopedic knowledge, which has well-defined relationships. This paper, however, focuses on commonsense knowledge. In this paper the authors introduce a “commonsense transformer” which trains on a knowledge base consisting of tuples and a pre-trained language model. Their trained model generates new nodes in the knowledge graph and completes phrases based on edges in the existing graph.</p>
<h2 id="training">Training</h2>
<p>The training data consists of tuples in the form of {subject, relation, object}, and the task is to generate the object given the subject and relation. The model architecture was the Vaswani GPT. The architecture is fairly straightforward for a transformer. The only difference of note is the representation of the knowledge tuple as a concatenated sequence of the words of each item of the tuple, along with a position embedding.</p>
<p>To train, the researchers train COMET with Atomic and ConceptNet as knowledge seed sets. The Atomic dataset contains 877k tuples of social commonsense knowledge. Experiments with Atomic used bleu-2 as an automatic evaluation metric. The researchers also used Amazon Mechanical Turk to identify whether the generated responses were reasonable.</p>
<p>The researchers also tested on ConceptNet. Relations were in the same {subject, relation, object} format. This dataset was evaluated using perplexity on the test set and accuracy of the generated positive samples on the pre-trained Bilinear AVG model.</p>
<h2 id="results">Results</h2>
<p>Atomic experiments showed that the model is able to generate coherent knowledge even when only 10% of the data in a domain is available. The researchers found that 91.7% of the greedily decoded ConceptNet examples were correct. The model seems to be producing fairly good knowledge tuples. However, sometimes the generated tuples are merely simplifications of previous knowledge.</p>
]]></content>
        </item>
        
        <item>
            <title>How to Create a Custom Pytorch Dataloader</title>
            <link>https://blog.lukesalamone.com/posts/pytorch-dataloader/</link>
            <pubDate>Thu, 28 Apr 2022 18:22:07 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/pytorch-dataloader/</guid>
            <description>&lt;p&gt;First, create a custom dataset class.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;from torch.utils.data import Dataset, DataLoader

class CustomDataset(Dataset):
  def __init__(self, features, labels):

    assert len(features) == len(labels)
    self.features = features
    self.labels = labels

  def __len__(self):
    return len(self.features)

  def __getitem__(self, idx):
    return self.features[idx], self.labels[idx]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create a custom dataloader where we specify the batch size.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;features, labels = load_data()

# features &amp;amp; labels must have equal lengths
# e.g. features = [[1,2,3],[4,5,6]]
#      labels = [7,8]

dataset = CustomDataset(features, labels)
dataloader = DataLoader(dataset,
                        batch_size=batch_size,
                        shuffle=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, iterate over the dataloader during training.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>First, create a custom dataset class.</p>
<pre><code class="language-python">from torch.utils.data import Dataset, DataLoader

class CustomDataset(Dataset):
  def __init__(self, features, labels):

    assert len(features) == len(labels)
    self.features = features
    self.labels = labels

  def __len__(self):
    return len(self.features)

  def __getitem__(self, idx):
    return self.features[idx], self.labels[idx]
</code></pre>
<p>Next, create a custom dataloader where we specify the batch size.</p>
<pre><code class="language-python">features, labels = load_data()

# features &amp; labels must have equal lengths
# e.g. features = [[1,2,3],[4,5,6]]
#      labels = [7,8]

dataset = CustomDataset(features, labels)
dataloader = DataLoader(dataset,
                        batch_size=batch_size,
                        shuffle=True)
</code></pre>
<p>Finally, iterate over the dataloader during training.</p>
<pre><code class="language-python">for epoch in range(num_epochs):
  for x, y in train_dataloader:
    # do stuff
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>How to Zip and Unzip a tar.gz File</title>
            <link>https://blog.lukesalamone.com/posts/how-to-tar-untar-file/</link>
            <pubDate>Wed, 30 Mar 2022 20:05:26 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/how-to-tar-untar-file/</guid>
            <description>&lt;p&gt;If you want to extract a tar archive&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-console&#34;&gt;tar -xf archive.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to compress a directory&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-console&#34;&gt;tar -czvf archive.tar.gz /path/to/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;rsquo;s all.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>If you want to extract a tar archive</p>
<pre><code class="language-console">tar -xf archive.tar.gz
</code></pre>
<p>If you want to compress a directory</p>
<pre><code class="language-console">tar -czvf archive.tar.gz /path/to/directory
</code></pre>
<p>That&rsquo;s all.</p>
]]></content>
        </item>
        
        <item>
            <title>Paper Summary: Defending Against Neural Fake News</title>
            <link>https://blog.lukesalamone.com/posts/grover-paper-summary/</link>
            <pubDate>Sun, 19 Sep 2021 20:13:09 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/grover-paper-summary/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://arxiv.org/abs/1905.12616&#34;&gt;&lt;em&gt;Defending Against Neural Fake News&lt;/em&gt;&lt;/a&gt; by Zellers et al. presents a model for controllable text generation called Grover. This model can be used to create highly believable computer-generated news articles. The authors present this paper as a method of detecting and preventing the spread of fake news. They claim their model is 92% accurate at detecting fake news stories, partially due to artifacts that generators include in the generated text.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><a href="https://arxiv.org/abs/1905.12616"><em>Defending Against Neural Fake News</em></a> by Zellers et al. presents a model for controllable text generation called Grover. This model can be used to create highly believable computer-generated news articles. The authors present this paper as a method of detecting and preventing the spread of fake news. They claim their model is 92% accurate at detecting fake news stories, partially due to artifacts that generators include in the generated text.</p>
<p>Grover functions in an adversarial manner:</p>
<ul>
<li>an “adversary” generates synthetic stories</li>
<li>a “discriminator” identifies fake stories</li>
</ul>
<p>Current generative models are fairly good at creating realistic-looking text, but largely lack the ability to be controlled via tunable parameters. In contrast, Grover models news stories as distributions like (<em>domain</em>, <em>date</em>, <em>authors</em>, <em>headline</em>, <em>body</em>) to be sampled from. Adversaries specify domain, date, and headline, and the generator creates the body and author as well as a more appropriate headline.</p>
<figure><img src="/img/grover-prob-distribution.png"
			alt="Grover samples from a joint distribution of the parts of a news article."><figcaption>
			<p>Grover samples from a joint distribution of the parts of a news article.</p>
		</figcaption>
</figure>

<p>Grover is built using a transformer architecture similar to BERT and GPT. Depending on the model size, the number of parameters varies from similar to GPT to on par with GPT-2. The authors use the RealNews corpus, resulting in 120 gigabytes of total file size. Training took 2 weeks on 256 TPU v3 cores.</p>
<figure><img src="/img/grover-vs-human.png"
			alt="Grover is actually better at writing synthetic articles than humans are according to authors."><figcaption>
			<p>Grover is actually better at writing synthetic articles than humans are according to authors.</p>
		</figcaption>
</figure>

<p>The results of the authors&rsquo; subjective experiments show that humans have a hard time identifying Grover-written propaganda. Using Mechanical Turk, articles were rated according to stylistic consistency, content sensibility, and overall trustworthiness. Grover, on the other hand, turns out to be a fairly good discriminator.</p>
<p>The authors also tested GPT2, BERT, and FastText on the task of classifying news as human or synthetic. The researchers set up the experiment in an unpaired setting (give a human/synthetic classification for a single article) and a paired setting (determine which is the human and which is the synthetic between 2 articles). Unsurprisingly, the paired setting was far easier. Also unsurprisingly, the larger models perform better at discriminating when paired with smaller generators.</p>
<figure><img src="/img/grover-discrimination-results.png"
			alt="For generators of the same size, discrimination accuracy was around 90-91%."><figcaption>
			<p>For generators of the same size, discrimination accuracy was around 90-91%.</p>
		</figcaption>
</figure>

<p>When comparing generator/discriminator pairs with the same numbers of parameters, classification accuracy was around 90-92%.
Grover tends to leave artifacts when generating text, and this fact may be part of the reason discriminators are so good at identifying synthetic text. For one, the authors identify “exposure bias” as one of the reasons. The fact that Grover is never trained on generated text, only on human-authored articles seems to contribute to this. Perplexity also tends to vary over the length of the generated article, and depending on the sampling variance may fall out of the distribution of human language.</p>
]]></content>
        </item>
        
        <item>
            <title>Connect Jupyter to Remote</title>
            <link>https://blog.lukesalamone.com/posts/connect-jupyter-to-remote/</link>
            <pubDate>Tue, 07 Sep 2021 09:10:56 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/connect-jupyter-to-remote/</guid>
            <description>&lt;p&gt;Here&amp;rsquo;s how to connect to a remote Jupyter notebook.&lt;/p&gt;
&lt;p&gt;Create an ssh tunnel to your remote machine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -L 8080:localhost:8080 user@12.34.56.78

# or use a .pem file to connect to ec2
ssh -L 8080:localhost:8080 -i &amp;quot;aws.pem&amp;quot; ec2-user@ec2-12-34-56-78.compute-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start Jupyter on that machine in headless mode:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jupyter notebook --no-browser --port=8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use a browser to open one of the urls that Jupyter presents:&lt;br&gt;
http://localhost:8080/?token=xyz&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Here&rsquo;s how to connect to a remote Jupyter notebook.</p>
<p>Create an ssh tunnel to your remote machine:</p>
<pre><code>ssh -L 8080:localhost:8080 user@12.34.56.78

# or use a .pem file to connect to ec2
ssh -L 8080:localhost:8080 -i &quot;aws.pem&quot; ec2-user@ec2-12-34-56-78.compute-1.amazonaws.com
</code></pre>
<p>Start Jupyter on that machine in headless mode:</p>
<pre><code>jupyter notebook --no-browser --port=8080
</code></pre>
<p>Use a browser to open one of the urls that Jupyter presents:<br>
http://localhost:8080/?token=xyz</p>
]]></content>
        </item>
        
        <item>
            <title>What is Marginalization?</title>
            <link>https://blog.lukesalamone.com/posts/what-is-marginalization/</link>
            <pubDate>Wed, 07 Jul 2021 14:23:12 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/what-is-marginalization/</guid>
            <description>&lt;p&gt;In machine learning and statistics, marginalization simply means summing over a set of independent variables. For example, suppose an avid tennis player kept track of the number of days he played tennis over a period of time as well as the weather on that day:&lt;/p&gt;
&lt;style&gt;
  .blue {
    background-color:#09f1;
  }
  .gray {
    background-color:#80808012;
  }
&lt;/style&gt;
&lt;table style=&#34;width:100%&#34;&gt;
  &lt;tr style=&#34;font-weight:bold&#34;&gt;
    &lt;th&gt;&lt;/th&gt;
    &lt;th&gt;&lt;/th&gt;
    &lt;th colspan=&#34;3&#34; style=&#34;text-align:center&#34;&gt;weather&lt;/th&gt;
    &lt;th&gt;&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr style=&#34;font-weight:bold; text-align:center; background-color: inherit&#34;&gt;
    &lt;td&gt;&lt;/td&gt;
    &lt;th&gt;&lt;/th&gt;
    &lt;td&gt;sunny&lt;/td&gt;
    &lt;td&gt;cloudy&lt;/td&gt;
    &lt;td&gt;rainy&lt;/td&gt;
    &lt;th&gt;totals&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
  	&lt;td rowspan=&#34;2&#34; style=&#34;font-weight:bold; text-align:right&#34;&gt;play?&lt;/td&gt;
    &lt;td style=&#34;text-align:right&#34;&gt;yes&lt;/td&gt;
    &lt;td class=&#34;blue&#34;&gt;70&lt;/td&gt;
    &lt;td class=&#34;blue&#34;&gt;25&lt;/td&gt;
    &lt;td class=&#34;blue&#34;&gt;1&lt;/td&gt;
    &lt;td class=&#34;gray&#34; style=&#34;font-weight:bold&#34;&gt;96&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr style=&#34;background-color: inherit;&#34;&gt;
  	&lt;td style=&#34;text-align:right&#34;&gt;no&lt;/td&gt;
  	&lt;td class=&#34;blue&#34;&gt;70&lt;/td&gt;
    &lt;td class=&#34;blue&#34;&gt;5&lt;/td&gt;
    &lt;td class=&#34;blue&#34;&gt;9&lt;/td&gt;
    &lt;td class=&#34;gray&#34; style=&#34;font-weight:bold&#34;&gt;84&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr style=&#34;font-weight:bold&#34;&gt;
  	&lt;td colspan=&#34;2&#34; style=&#34;text-align:right&#34;&gt;totals&lt;/td&gt;
  	&lt;td class=&#34;gray&#34;&gt;140&lt;/td&gt;
    &lt;td class=&#34;gray&#34;&gt;30&lt;/td&gt;
    &lt;td class=&#34;gray&#34;&gt;10&lt;/td&gt;
    &lt;td class=&#34;gray&#34;&gt;180&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;(&lt;em&gt;In this table we&amp;rsquo;re keeping track of the number of days. If you want probabilities, divide each value in the table by 180. But I think whole numbers are easier to think about so I&amp;rsquo;m keeping them.&lt;/em&gt;)&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>In machine learning and statistics, marginalization simply means summing over a set of independent variables. For example, suppose an avid tennis player kept track of the number of days he played tennis over a period of time as well as the weather on that day:</p>
<style>
  .blue {
    background-color:#09f1;
  }
  .gray {
    background-color:#80808012;
  }
</style>
<table style="width:100%">
  <tr style="font-weight:bold">
    <th></th>
    <th></th>
    <th colspan="3" style="text-align:center">weather</th>
    <th></th>
  </tr>
  <tr style="font-weight:bold; text-align:center; background-color: inherit">
    <td></td>
    <th></th>
    <td>sunny</td>
    <td>cloudy</td>
    <td>rainy</td>
    <th>totals</th>
  </tr>
  <tr>
  	<td rowspan="2" style="font-weight:bold; text-align:right">play?</td>
    <td style="text-align:right">yes</td>
    <td class="blue">70</td>
    <td class="blue">25</td>
    <td class="blue">1</td>
    <td class="gray" style="font-weight:bold">96</td>
  </tr>
  <tr style="background-color: inherit;">
  	<td style="text-align:right">no</td>
  	<td class="blue">70</td>
    <td class="blue">5</td>
    <td class="blue">9</td>
    <td class="gray" style="font-weight:bold">84</td>
  </tr>
  <tr style="font-weight:bold">
  	<td colspan="2" style="text-align:right">totals</td>
  	<td class="gray">140</td>
    <td class="gray">30</td>
    <td class="gray">10</td>
    <td class="gray">180</td>
  </tr>
</table>
<p>(<em>In this table we&rsquo;re keeping track of the number of days. If you want probabilities, divide each value in the table by 180. But I think whole numbers are easier to think about so I&rsquo;m keeping them.</em>)</p>
<p>To marginalize one of the variables, we just sum one of the variables. For example, to marginalize the weather, we would sum each of the rows to find that 96/180=53% of the time tennis was played, and 47% of the time tennis was not played. Likewise, to marginalize the boolean variable of whether tennis was played, we just sum the columns: no matter whether tennis was played on that day, how many days was it sunny? 140.</p>
<p>Another way of saying this is the marginal distribution of sunny weather is the first column (containing 70 and 70). The marginal distribution of playing tennis is the first row (containing 70, 25, and 1).</p>
]]></content>
        </item>
        
        <item>
            <title>Colab: Connect to Google Drive</title>
            <link>https://blog.lukesalamone.com/posts/connect-to-colab/</link>
            <pubDate>Wed, 30 Jun 2021 22:58:18 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/connect-to-colab/</guid>
            <description>&lt;p&gt;Here&amp;rsquo;s how to connect your Google Colab notebook to your Drive directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;from google.colab import drive
drive.mount(&#39;/content/gdrive&#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Follow the prompts from there. That is all.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Here&rsquo;s how to connect your Google Colab notebook to your Drive directory:</p>
<pre><code class="language-python">from google.colab import drive
drive.mount('/content/gdrive')
</code></pre>
<p>Follow the prompts from there. That is all.</p>
]]></content>
        </item>
        
        <item>
            <title>BERT vs GPT-2 Performance</title>
            <link>https://blog.lukesalamone.com/posts/bert-vs-gpt2/</link>
            <pubDate>Mon, 21 Jun 2021 01:04:42 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/bert-vs-gpt2/</guid>
            <description>&lt;p&gt;There are quite a few BERT vs GPT-2 breakdowns online, mostly focusing on the architectural differences between the two models. However, I am more interested in the performance differences between the two models, specifically their predictive capabilities. This blog post outlines the results of my experiments. If you&amp;rsquo;re interested in GPT-3&amp;rsquo;s performance on image recognition, &lt;a href=&#34;../posts/chatgpt-mnist/&#34;&gt;I also tested that&lt;/a&gt; later on.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/lukesalamone/gpt2-vs-bert&#34;&gt;The code used in this experiment can be found on my Github&lt;/a&gt;.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>There are quite a few BERT vs GPT-2 breakdowns online, mostly focusing on the architectural differences between the two models. However, I am more interested in the performance differences between the two models, specifically their predictive capabilities. This blog post outlines the results of my experiments. If you&rsquo;re interested in GPT-3&rsquo;s performance on image recognition, <a href="/posts/chatgpt-mnist/">I also tested that</a> later on.</p>
<p><a href="https://github.com/lukesalamone/gpt2-vs-bert">The code used in this experiment can be found on my Github</a>.</p>
<h2 id="bert">BERT</h2>
<p>The <a href="https://arxiv.org/pdf/1810.04805.pdf">Devlin et al. model</a> was released in November 2018. It is a transformer-based language model pretrained on masked input (also known as the <em>cloze</em> task). During pretraining, 15% of tokens are hidden from the model, and it is trained to predict the masked tokens. As a result, I was able to evaluate its ability to correctly predict a masked token at a random position in a fixed-size input.</p>
<p>I looked at the following varieties of BERT:</p>
<table>
	<thead>
			<tr>
					<th>Model</th>
					<th># Parameters</th>
					<th>Compare to</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>bert-base-uncased</td>
					<td>110 million</td>
					<td>gpt2</td>
			</tr>
			<tr>
					<td>bert-base-cased</td>
					<td>109 million</td>
					<td>gpt2</td>
			</tr>
			<tr>
					<td>bert-large-uncased</td>
					<td>336 million</td>
					<td>gpt2-medium</td>
			</tr>
			<tr>
					<td>bert-large-cased</td>
					<td>335 million</td>
					<td>gpt2-medium</td>
			</tr>
	</tbody>
</table>
<p>This table also includes corresponding GPT-2 models which have a similar number of parameters. <a href="https://huggingface.co/transformers/pretrained_models.html">Source</a></p>
<h2 id="gpt-2">GPT-2</h2>
<p>The <a href="https://d4mucfpksywv.cloudfront.net/better-language-models/language_models_are_unsupervised_multitask_learners.pdf">Radford et al. model</a> hit the scene in February of 2019. Like BERT it is a transformer-based  model, and comes in various sizes ranging from 117M parameters up to 1.5B parameters (gpt2-xl). Because GPT-2 is an autoregressive model, experiments with this family of models perform one token of generation following input context, comparing with the target token for accuracy measurement.</p>
<p>Here we will be evaluating two flavors of this model:</p>
<table>
	<thead>
			<tr>
					<th>Model</th>
					<th># Parameters</th>
					<th>Compare to</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>gpt2</td>
					<td>117 million</td>
					<td>bert-base</td>
			</tr>
			<tr>
					<td>gpt2-medium</td>
					<td>345 million</td>
					<td>bert-large</td>
			</tr>
	</tbody>
</table>
<p>This table also includes corresponding BERT models which have a similar number of parameters. <a href="https://huggingface.co/transformers/pretrained_models.html">Source</a></p>
<h2 id="wikitext-token-prediction">Wikitext Token prediction</h2>
<p>To evaluate the models, I sampled 10,000 random sequences from <a href="https://paperswithcode.com/dataset/wikitext-2">Wikitext-2</a>.</p>
<p><strong>For BERT</strong>, a random sequence of 100 tokens is selected. Then, for each sequence, a random position within that sequence is selected and masked. BERT will be required to predict this token, so accuracy is measured as the percentage of the time which its masked token is predicted correctly.</p>
<p><strong>For GPT-2</strong>, a random sequence of 100 tokens is selected. Then, for each sequence, a random position within that sequence is selected. Because GPT-2 is autoregressive, it cannot attend to tokens on the right, so the sequence is truncated at the selected position. The sequence is then padded appropriately to maintain a fixed sequence length of 100.</p>
<p>Below we can see the performance of all 6 models on these tasks. The data has been smoothed by bucketing into groups of 5 positions at once (i.e. positions 0-4, 5-9, etc). You can see that performance of GPT-2 continues to rise as it is given additional context, while BERT models are relatively stable after being given around 5 tokens of context. Interestingly, BERT performance drops off quite steeply over the last 5-10 token positions.</p>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.4.0/dist/chart.min.js" type="text/javascript"></script>
<script src="/js/bert-vs-gpt2.js"></script>
<script src="/js/util.js"></script>
<div id="graph1"><canvas></canvas></div>
<p>When we zoom in on the final 10 positions, things start to get interesting. Both varieties of GPT-2 actually beat out all varieties of BERT at the final position.</p>
<div id="graph2"><canvas></canvas></div>
<h2 id="conclusion">Conclusion</h2>
<p>BERT and GPT-2 perform quite differently on the token prediction task depending on the position of the token being predicted. For a fixed sequence length of 100 tokens, BERT performs best when the masked token is between positions 5 and 95, while GPT-2 tends to continually improve as context length increases. Interestingly, when the final token in the sequence is to be predicted, BERT&rsquo;s performance falls off dramatically, while GPT-2 performance remains stable.</p>
]]></content>
        </item>
        
        <item>
            <title>How does GPT-2 Tokenize Text?</title>
            <link>https://blog.lukesalamone.com/posts/gpt2-tokenization/</link>
            <pubDate>Thu, 17 Jun 2021 19:30:48 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/gpt2-tokenization/</guid>
            <description>&lt;p&gt;Let&amp;rsquo;s explore how GPT-2 tokenizes text.&lt;/p&gt;
&lt;h2 id=&#34;what-is-tokenization&#34;&gt;What is tokenization?&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s important to understand that GPT-2 doesn&amp;rsquo;t work with strings directly. Instead, it needs to tokenize the input string, which is essentially a process for converting the string into a list of numbers, or &amp;ldquo;tokens&amp;rdquo;. It is these tokens which are passed into the model during training or for inference. As a concrete example, let&amp;rsquo;s look at a few sample sentences:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Let&rsquo;s explore how GPT-2 tokenizes text.</p>
<h2 id="what-is-tokenization">What is tokenization?</h2>
<p>It&rsquo;s important to understand that GPT-2 doesn&rsquo;t work with strings directly. Instead, it needs to tokenize the input string, which is essentially a process for converting the string into a list of numbers, or &ldquo;tokens&rdquo;. It is these tokens which are passed into the model during training or for inference. As a concrete example, let&rsquo;s look at a few sample sentences:</p>
<pre><code class="language-python">tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
tokens1 = tokenizer('I love my dog')
</code></pre>
<p>When we look at <code>tokens1</code> we see there are 4 tokens:</p>
<pre><code class="language-python">{'input_ids': [40, 1842, 616, 3290], 'attention_mask': [1, 1, 1, 1]}
</code></pre>
<p>Here what we care about is the <code>'input_ids'</code> list. We can ignore the <code>'attention_mask'</code> for now. We can convert the tokens in <code>[40, 1842, 616, 3290]</code> back into strings using <code>tokenizer.decode</code>:</p>
<pre><code class="language-python">tokens1 = tokens1['input_ids']

[tokenizer.decode(x) for x in tokens1]
# prints ['I', ' love', ' my', ' dog']

[tokenizer.decode(x).strip().lower() for x in tokens1]
# prints ['i', 'love', 'my', 'dog']
</code></pre>
<p>This process allows us to recover the tokens as strings from the tokenizer. For dictionary lookups, we&rsquo;ll also lowercase the strings and remove the whitespace from them.</p>
<p>Now, let&rsquo;s see what happens when we do the same thing with more complex words:</p>
<pre><code class="language-python">tokens2 = tokenizer('My favorite color is chartreuse')['token_ids']
[tokenizer.decode(x).strip().lower() for x in tokens2]
# prints ['my', 'favorite', 'color', 'is', 'chart', 're', 'use']
</code></pre>
<p>Because &ldquo;chartreuse&rdquo; isn&rsquo;t in GPT-2&rsquo;s vocabulary, it is tokenized as &ldquo;chart&rdquo;, &ldquo;re&rdquo; and &ldquo;use&rdquo;.</p>
<h3 id="about-that-attention-mask">About that attention mask</h3>
<p>For brevity I glossed over what <code>attention_mask</code> does above. If you&rsquo;re interested in attention masks, <a href="/posts/what-are-attention-masks/">I have a blog post on that very topic</a>!</p>
<h2 id="english-words">English words</h2>
<p>Now it would be interesting to see how many tokens in GPT-2&rsquo;s vocabulary are actually English words. This is an imprecise metric since it depends heavily on which dictionary we use. (There is no single authoritative source of all English words.) I&rsquo;ll use several dictionaries and compare the results.</p>
<h3 id="enchant">Enchant</h3>
<p><a href="https://pyenchant.github.io/pyenchant/tutorial.html">PyEnchant</a> contains a python module <code>enchant</code> which we can use to check if a word is spelled correctly. It can also make spelling suggestions for incorrectly spelled words:</p>
<pre><code class="language-python">import enchant
d = enchant.request_dict(&quot;en_US&quot;)
d.check('Hello')
# prints True

d.check('Helo')
# prints False
</code></pre>
<h3 id="nltk-words">NLTK words</h3>
<p>The popular NLP library <a href="https://www.nltk.org/">NLTK</a> also contains a word list, accessible through its <code>corpus</code> module.</p>
<pre><code class="language-python">from nltk.corpus import words

nltk_words = set(words.words())
len(nltk_words)
# prints 235892
</code></pre>
<h3 id="english-350k">English 350k</h3>
<p>This list of words was taken from this <a href="https://github.com/dwyl/english-words/blob/master/words_alpha.txt">github repository</a>. It is a convenient list of lowercased words containing only letters. It seems to be the biggest of the word lists.</p>
<h3 id="lemmatization">Lemmatization</h3>
<p>We can bump our numbers up slightly through <a href="https://en.wikipedia.org/wiki/Lemmatisation">lemmatization</a>:</p>
<blockquote>
<p>In many languages, words appear in several inflected forms. For example, in English, the verb &rsquo;to walk&rsquo; may appear as &lsquo;walk&rsquo;, &lsquo;walked&rsquo;, &lsquo;walks&rsquo; or &lsquo;walking&rsquo;. The base form, &lsquo;walk&rsquo;, that one might look up in a dictionary, is called the lemma for the word. The association of the base form with a part of speech is often called a lexeme of the word.</p>
</blockquote>
<p>For our lemmatizer we will use <code>WordNetLemmatizer</code> from <code>nltk.stem.wordnet</code>.</p>
<h2 id="testing-gpt-2-tokens">Testing GPT-2 tokens</h2>
<p>So of the tokens which GPT-2 uses, how many are English words? We can break this down metric by the dictionary used.</p>
<table>
	<thead>
			<tr>
					<th>Dictionary</th>
					<th>% Words</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>English370k †</td>
					<td>72.92%</td>
			</tr>
			<tr>
					<td>English370k</td>
					<td>72.59%</td>
			</tr>
			<tr>
					<td>Enchant †</td>
					<td>60.48%</td>
			</tr>
			<tr>
					<td>Enchant</td>
					<td>60.17%</td>
			</tr>
			<tr>
					<td>NLTK words †</td>
					<td>57.07%</td>
			</tr>
			<tr>
					<td>NLTK words</td>
					<td>48.27%</td>
			</tr>
	</tbody>
</table>
<p><em>† indicates words were lemmatized</em></p>
<p>So the English370k word list seems to capture the most tokens from the three dictionaries. Also note the mild impact of lemmatization: although it may bump some of the percentages up a bit, it&rsquo;s not enough for one dictionary to outperform another.</p>
<div id="pie"><canvas></canvas></div>
<p>Looking at the tokens which aren&rsquo;t in the dictionary, around 73% of them are non-word alphabetical strings. The final 27% is accounted for by symbols, numbers, and non-ascii character sequences (unicode characters from languages like Arabic, Korean, and Chinese). If we remove these, we end up with about 10k tokens containing only letters, which is around 21% of GPT-2&rsquo;s total vocabulary. I&rsquo;ve included this list in a <a href="https://gist.github.com/lukesalamone/22ce6f362db3bdd09eda3cc5cbf5576f">github gist</a> (duplicates removed).</p>
<h2 id="now-what">Now what?</h2>
<p>Looking at these non-word alphabetical strings, it&rsquo;s interesting to see how the Internet (as GPT-2 saw it) was encoded. Then again, it also contains a lot of proper nouns which wouldn&rsquo;t be in a normal dictionary like &ldquo;starbucks&rdquo;.</p>
<p>Other tokens are clearly vestiges of the scraping process which was used to gather text which GPT-2 trained on. Tokens like &ldquo;rawdownloadcloneembedreportprint&rdquo;, &ldquo;buyableinstoreandonline&rdquo;, &ldquo;randomredditorwithno&rdquo;, and &ldquo;itemthumbnailimage&rdquo; contain next to zero semantic value and the vocabulary space would probably have been better served with more meaningful tokens.</p>
<p>The following are the longest non-dictionary tokens found in GPT-2&rsquo;s vocabulary:</p>
<table>
	<thead>
			<tr>
					<th>Token ID</th>
					<th>String</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>39177</td>
					<td>ItemThumbnailImage</td>
			</tr>
			<tr>
					<td>30210</td>
					<td>guiActiveUnfocused</td>
			</tr>
			<tr>
					<td>39755</td>
					<td>isSpecialOrderable</td>
			</tr>
			<tr>
					<td>31576</td>
					<td>externalActionCode</td>
			</tr>
			<tr>
					<td>39753</td>
					<td>quickShipAvailable</td>
			</tr>
			<tr>
					<td>39757</td>
					<td>channelAvailability</td>
			</tr>
			<tr>
					<td>36174</td>
					<td>RandomRedditorWithNo</td>
			</tr>
			<tr>
					<td>30899</td>
					<td>cloneembedreportprint</td>
			</tr>
			<tr>
					<td>40242</td>
					<td>BuyableInstoreAndOnline</td>
			</tr>
			<tr>
					<td>30906</td>
					<td>rawdownloadcloneembedreportprint</td>
			</tr>
	</tbody>
</table>
<p>We may also be able to measure performance of GPT-2 on certain tasks based on how many of the tokens were dictionary words. It might be true, for example, that sentences with higher proportions of dictionary word tokens would perform better on sentence completion tasks.</p>
<script src="/js/chart.min.js" type="text/javascript"></script>
<script src="/js/gpt2-tokens-pie.js" type="text/javascript"></script>
]]></content>
        </item>
        
        <item>
            <title>What Are Attention Masks?</title>
            <link>https://blog.lukesalamone.com/posts/what-are-attention-masks/</link>
            <pubDate>Tue, 15 Jun 2021 19:09:36 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/what-are-attention-masks/</guid>
            <description>&lt;p&gt;TLDR: Attention masks allow us to send a batch into the transformer even when the examples in the batch have varying lengths. We do this by padding all sequences to the same length, then using the &amp;ldquo;attention_mask&amp;rdquo; tensor to identify which tokens are padding.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;../img/attention_mask.png&#34;
			alt=&#34;Here we use a batch with three samples padded from the left since we want to predict the next token on the right. (Padding on the right would probably predict another pad.)&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;Here we use a batch with three samples padded from the left since we want to predict the next token on the right. (Padding on the right would probably predict another pad.)&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;</description>
            <content type="html"><![CDATA[<p>TLDR: Attention masks allow us to send a batch into the transformer even when the examples in the batch have varying lengths. We do this by padding all sequences to the same length, then using the &ldquo;attention_mask&rdquo; tensor to identify which tokens are padding.</p>
<figure><img src="/img/attention_mask.png"
			alt="Here we use a batch with three samples padded from the left since we want to predict the next token on the right. (Padding on the right would probably predict another pad.)"><figcaption>
			<p>Here we use a batch with three samples padded from the left since we want to predict the next token on the right. (Padding on the right would probably predict another pad.)</p>
		</figcaption>
</figure>

<p>If you want to perform inference with transformers one sequence at a time, you can ignore attention masks. The &ldquo;slow way&rdquo; will be sufficient for your needs.</p>
<h2 id="the-slow-way">The slow way</h2>
<p>We can <a href="/posts/gpt2-tokenization/">tokenize and perform inference with GPT-2</a> one sequence at a time, but it&rsquo;s slow:</p>
<pre><code class="language-python">from transformers import GPT2LMHeadModel, GPT2Tokenizer

tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
gpt2 = GPT2LMHeadModel.from_pretrained('gpt2')

context = tokenizer('It will rain in the', return_tensors='pt')

prediction = gpt2.generate(**context, max_length=10)
tokenizer.decode(prediction[0])
# prints 'It will rain in the morning, and the rain'
</code></pre>
<p>It&rsquo;s way faster to batch the inputs, which means adding their token vectors to the context and performing inference only once.</p>
<h2 id="the-un-slow-way">The un-slow way</h2>
<p>The cool way to perform inference on many samples is with batching. It&rsquo;s much faster but it&rsquo;s also slightly more complicated.</p>
<pre><code class="language-python">tokenizer.padding_side = &quot;left&quot;
tokenizer.pad_token = tokenizer.eos_token

sentences = [&quot;It will rain in the&quot;,
            &quot;I want to eat a big bowl of&quot;,
            &quot;My dog is&quot;]
inputs = tokenizer(sentences, return_tensors=&quot;pt&quot;, padding=True)

output_sequences = gpt2.generate(**inputs)

for seq in output_sequences:
    print(tokenizer.decode(seq))
</code></pre>
<p>What&rsquo;s happening here? And what does this have to do with attention masks? First let&rsquo;s explain padding, then take a look at the code line by line.</p>
<p>We feed tokens into transformer-based language models like GPT-2 and BERT for inference as <a href="https://pytorch.org/docs/stable/tensors.html">tensors</a>. A tensor is like a python list but with a few extra features and restrictions. Specifically, for a tensor of dimension 2+, all vectors in that dimension need to be the same length. For example,</p>
<pre><code class="language-python">from torch import tensor

tensor([[1,2], [3,4]])  # ok
tensor([[1,2], [3]])   # error!
</code></pre>
<p>When we tokenize an input, it it will be turned into a tensor containing sequence of integers, each corresponding to an item in the transformer&rsquo;s vocabulary. Here is an example tokenization in GPT-2:</p>
<table>
	<thead>
			<tr>
					<th>String</th>
					<th>Token ID</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>It</td>
					<td>1026</td>
			</tr>
			<tr>
					<td>will</td>
					<td>481</td>
			</tr>
			<tr>
					<td>rain</td>
					<td>6290</td>
			</tr>
			<tr>
					<td>in</td>
					<td>287</td>
			</tr>
			<tr>
					<td>the</td>
					<td>262</td>
			</tr>
	</tbody>
</table>
<p>Suppose we wanted to include a second sequence in our input:</p>
<table>
	<thead>
			<tr>
					<th>String</th>
					<th>Token ID</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>My</td>
					<td>3666</td>
			</tr>
			<tr>
					<td>dog</td>
					<td>3290</td>
			</tr>
			<tr>
					<td>is</td>
					<td>318</td>
			</tr>
	</tbody>
</table>
<p>Because these two sequences have different lengths, we can&rsquo;t just combine them in one tensor. Instead, we have to <em>pad</em> the shorter sequences with dummy tokens so that each sequence is the same length. And because we want the model to continue to add to the right side of our sequence, we will pad the left side of shorter sequences.</p>
<table>
	<thead>
			<tr>
					<th>String</th>
					<th>Token ID</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>&lt;pad&gt;</code></td>
					<td>50256</td>
			</tr>
			<tr>
					<td>My</td>
					<td>3666</td>
			</tr>
			<tr>
					<td>dog</td>
					<td>3290</td>
			</tr>
			<tr>
					<td>is</td>
					<td>318</td>
			</tr>
	</tbody>
</table>
<p>This is where the attention mask comes in. The attention mask simply shows the transformer which tokens are padding, placing 0s in the positions of padding tokens and 1s in the positions of actual tokens. Now that we understand that, let&rsquo;s look at the code line by line.</p>
<pre><code class="language-python">tokenizer.padding_side = &quot;left&quot;
</code></pre>
<p>This line tells the tokenizer to begin padding from the left (default is right) because the logits of the rightmost token will be used to predict future tokens.</p>
<pre><code class="language-python">tokenizer.pad_token = tokenizer.eos_token
</code></pre>
<p>This line specifies which token we will use for padding. It doesn&rsquo;t matter which one you choose, but here we&rsquo;re choosing the &ldquo;end of sequence&rdquo; token.</p>
<pre><code class="language-python">sentences = [&quot;It will rain in the&quot;,
            &quot;I want to eat a big bowl of&quot;,
            &quot;My dog is&quot;]
</code></pre>
<p>These three sequences all have different lengths when tokenized, so should be a good test of our padding method.</p>
<pre><code class="language-python">inputs = tokenizer(sentences, return_tensors=&quot;pt&quot;, padding=True)
</code></pre>
<p>Now we tokenize. We&rsquo;re passing in the sentences from above, telling the tokenizer to use PyTorch tensors (rather than Tensorflow), and telling the tokenizer to add padding for us. We can print <code>inputs</code> here to confirm that, yes, tokenization is working as we thought:</p>
<pre><code class="language-python">{'input_ids': tensor([
    [50256, 50256, 50256,  1026,   481,  6290,   287,   262],
    [   40,   765,   284,  4483,   257,  1263,  9396,   286],
    [50256, 50256, 50256, 50256, 50256,  3666,  3290,   318]
  ]),
'attention_mask': tensor([
    [0, 0, 0, 1, 1, 1, 1, 1],
    [1, 1, 1, 1, 1, 1, 1, 1],
    [0, 0, 0, 0, 0, 1, 1, 1]
  ])}
</code></pre>
<p>As you can see, the first and third sequence include padding at the beginning, and the <code>attention_mask</code> parameter marks the position of this padding.</p>
<p>Now let&rsquo;s actually pass this input into the model to generate new text:</p>
<pre><code class="language-python">output_sequences = gpt2.generate(**inputs)
</code></pre>
<p>If you&rsquo;re unfamiliar with <code>**kwargs</code> syntax for function calls, this passes in the <code>inputs</code> dict as named parameters, using the keys as the parameter names and the values as the corresponding argument values. <a href="https://docs.python.org/3/tutorial/controlflow.html#keyword-arguments">Check the docs for more info</a>.</p>
<p>Finally, we just need to loop through each of the generated sequences and print out the result in human readable form, using the <code>decode()</code> function to convert token IDs to strings.</p>
<pre><code class="language-python">for seq in output_sequences:
    print(tokenizer.decode(seq))
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>How Does Convolution Work?</title>
            <link>https://blog.lukesalamone.com/posts/how-does-convolution-work/</link>
            <pubDate>Mon, 14 Jun 2021 21:05:06 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/how-does-convolution-work/</guid>
            <description>&lt;p&gt;Convolutional neural networks have had breakthrough success in image recognition, natural language processing, and even board games like Chess and Go. But what&amp;rsquo;s really going on during convolution? Well, I think the easiest way to explain is with an interactive demo. Feel free to play around with the parameters below to see for yourself!&lt;/p&gt;
&lt;script src=&#34;../js/util.js&#34;&gt;&lt;/script&gt;
&lt;script src=&#34;../js/convolution-demo.js&#34;&gt;&lt;/script&gt;
&lt;link rel=&#34;stylesheet&#34; href=&#34;../css/convolution-demo.css&#34; /&gt;
&lt;div id=&#34;input-output&#34;&gt;
  &lt;div id=&#34;input-grid&#34;&gt;&lt;/div&gt;
  &lt;div id=&#34;output-grid&#34;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;controls&#34;&gt;
  &lt;table&gt;
    &lt;tr id=&#34;number&#34;&gt;
      &lt;td&gt;number:&lt;/td&gt;
      &lt;td&gt;
        &lt;select&gt;
          &lt;option value=&#34;four&#34;&gt;four&lt;/option&gt;
          &lt;option value=&#34;three&#34;&gt;three&lt;/option&gt;
          &lt;option value=&#34;eight&#34;&gt;eight&lt;/option&gt;
        &lt;/select&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id=&#34;padding&#34;&gt;
      &lt;td&gt;padding: &lt;span class=&#34;val&#34;&gt;&lt;/span&gt;&lt;/td&gt;
      &lt;td&gt;&lt;input type=&#34;range&#34; min=&#34;0&#34; max=&#34;2&#34; value=&#34;0&#34;&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id=&#34;kernelsize&#34;&gt;
      &lt;td&gt;kernel size: &lt;span class=&#34;val&#34;&gt;&lt;/span&gt;&lt;/td&gt;
      &lt;td&gt;&lt;input type=&#34;range&#34; min=&#34;1&#34; max=&#34;4&#34; value=&#34;2&#34;&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id=&#34;stride&#34;&gt;
      &lt;td&gt;stride: &lt;span class=&#34;val&#34;&gt;&lt;/span&gt;&lt;/td&gt;
      &lt;td&gt;&lt;input type=&#34;range&#34; min=&#34;1&#34; max=&#34;3&#34; value=&#34;1&#34;&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id=&#34;speed&#34;&gt;
      &lt;td&gt;speed: &lt;span class=&#34;val&#34;&gt;&lt;/span&gt;&lt;/td&gt;
      &lt;td&gt;&lt;input type=&#34;range&#34; min=&#34;1&#34; max=&#34;5&#34; value=&#34;3&#34;&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr id=&#34;errors&#34; style=&#34;display:none&#34;&gt;
      &lt;td colspan=&#34;2&#34;&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;You can use the settings above to control the hyperparameters of the convolutional layer.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Convolutional neural networks have had breakthrough success in image recognition, natural language processing, and even board games like Chess and Go. But what&rsquo;s really going on during convolution? Well, I think the easiest way to explain is with an interactive demo. Feel free to play around with the parameters below to see for yourself!</p>
<script src="/js/util.js"></script>
<script src="/js/convolution-demo.js"></script>
<link rel="stylesheet" href="/css/convolution-demo.css" />
<div id="input-output">
  <div id="input-grid"></div>
  <div id="output-grid"></div>
</div>
<div id="controls">
  <table>
    <tr id="number">
      <td>number:</td>
      <td>
        <select>
          <option value="four">four</option>
          <option value="three">three</option>
          <option value="eight">eight</option>
        </select>
      </td>
    </tr>
    <tr id="padding">
      <td>padding: <span class="val"></span></td>
      <td><input type="range" min="0" max="2" value="0"></td>
    </tr>
    <tr id="kernelsize">
      <td>kernel size: <span class="val"></span></td>
      <td><input type="range" min="1" max="4" value="2"></td>
    </tr>
    <tr id="stride">
      <td>stride: <span class="val"></span></td>
      <td><input type="range" min="1" max="3" value="1"></td>
    </tr>
    <tr id="speed">
      <td>speed: <span class="val"></span></td>
      <td><input type="range" min="1" max="5" value="3"></td>
    </tr>
    <tr id="errors" style="display:none">
      <td colspan="2"></td>
    </tr>
  </table>
</div>
<p>You can use the settings above to control the hyperparameters of the convolutional layer.</p>
<p>As you&rsquo;ve probably figured out, the left side shows the input, a 20x20 handwritten digit, and the right side shows the output as it is being drawn. A convolutional layer will pass a filter over the input, selecting only a small group of pixels at a time.</p>
<p>Not shown: once the filter selects a window of pixels, it will compute a dot product with the pixel values and its own weights. In this demo the filter weights are assumed to be 1 for simplicity.</p>
<h2 id="more-on-the-hyperparameters">More on the hyperparameters</h2>
<p>Sometimes the settings will result in an invalid convolution. This happens if the kernel extends beyond the activation map. We can write a python function to check whether it will be valid:</p>
<pre><code class="language-python">def valid_convolution(input_size, kernel_size, padding, stride):
  if (input_size + 2*padding - kernel_size) % stride == 0:
    return True
  else:
    return False
</code></pre>
<h3 id="padding">Padding</h3>
<p>Increasing the padding will add extra pixels, typically zeros, around the edges of the input. This is done in order to ensure that the layer parameters will be valid. Additionally, it causes pixels which would have been on the edges to be more to the middle of the activation map.</p>
<h3 id="kernel-size">Kernel Size</h3>
<p>This refers to the &ldquo;sliding window&rdquo; which passes over the input. Choosing a larger kernel tends to increase the &ldquo;smudging&rdquo; which happens in the output, while a smaller kernel preserves more information for deeper layers. Sometimes kernel size is referred to as filter size.</p>
<h3 id="stride">Stride</h3>
<p>Stride refers to the distance the sliding window will move between steps. A stride of 1 moves the window one pixel to the right, while a stride of 2 moves the window 2 pixels. A larger stride can reduce output layer size at the expense of granularity.</p>
]]></content>
        </item>
        
        <item>
            <title>Python: Serve an HTML File</title>
            <link>https://blog.lukesalamone.com/posts/python-serve-html/</link>
            <pubDate>Sun, 09 May 2021 15:06:11 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/python-serve-html/</guid>
            <description>&lt;p&gt;If you want to serve some HTML with python run&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-console&#34;&gt;python -m http.server 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then navigate to &lt;a href=&#34;http://localhost:8000/&#34;&gt;http://localhost:8000&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is not meant for production environments but will get you around CORS restrictions that would come from simply opening a local file in your browser.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>If you want to serve some HTML with python run</p>
<pre><code class="language-console">python -m http.server 8000
</code></pre>
<p>Then navigate to <a href="http://localhost:8000/">http://localhost:8000</a>.</p>
<p>This is not meant for production environments but will get you around CORS restrictions that would come from simply opening a local file in your browser.</p>
]]></content>
        </item>
        
        <item>
            <title>How to Train and Run a Simple Language Model</title>
            <link>https://blog.lukesalamone.com/posts/running-simple-language-model/</link>
            <pubDate>Fri, 16 Apr 2021 21:08:53 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/running-simple-language-model/</guid>
            <description>&lt;p&gt;This article will show how to run a simple language model, KenLM. It&amp;rsquo;s not as powerful as transformer-based models like BERT or GPT-3, but depending on what you&amp;rsquo;re trying to accomplish it may be more than enough. This tutorial should take you about 15 minutes, including the time to run the scripts.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s work backwards from where we&amp;rsquo;re trying to get to. When you&amp;rsquo;ve finished, you should be able to run the following script:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>This article will show how to run a simple language model, KenLM. It&rsquo;s not as powerful as transformer-based models like BERT or GPT-3, but depending on what you&rsquo;re trying to accomplish it may be more than enough. This tutorial should take you about 15 minutes, including the time to run the scripts.</p>
<p>Let&rsquo;s work backwards from where we&rsquo;re trying to get to. When you&rsquo;ve finished, you should be able to run the following script:</p>
<pre><code class="language-python">import kenlm

path = 'path/to/model.arpa'
lm = kenlm.LanguageModel(path)

sentence = &quot;I am not superstitious but I am a little stitious&quot;
print(model.score(sentence))
</code></pre>
<p>The first step will be to build KenLM. Then, we will build the ARPA file which KenLM uses to evaluate.</p>
<h2 id="building-kenlm">Building KenLM</h2>
<p>First, clone this repository:</p>
<p><code>git clone git@github.com:kpu/kenlm.git</code></p>
<p>Now we need to build the KenLM toolkit. Run the following to build:</p>
<pre><code>mkdir -p build
cd build
cmake ..
make -j 4
</code></pre>
<p>Now we just need to provide the model with a <code>.arpa</code> ngram language model file. So let&rsquo;s get one.</p>
<h2 id="building-an-ngram-language-model-from-wikitext-2">Building an ngram language model from Wikitext-2</h2>
<p>First, let&rsquo;s clone a repository which will build an ARPA file. This repository builds the ngram file from <a href="https://paperswithcode.com/dataset/wikitext-2">Wikitext-2</a>, a common dataset used in natural language processing.</p>
<pre><code>git clone git@github.com:daandouwe/ngram-lm.git
cd ngram-lm
mkdir data
./get-data.sh
mkdir arpa
./main.py --order 3 --interpolate --save-arpa --name wiki-interpolate
</code></pre>
<p>Once that has finished, you&rsquo;ll have new <code>.arpa</code> in the <code>arpa</code> directory you created. This script took the longest to run on my machine. Be patient, your computer is busy reading all of Wikipedia.</p>
<h2 id="all-together-now">All Together Now</h2>
<p>Now we&rsquo;re finally ready to evaluate a sentence with the language model.</p>
<pre><code class="language-python">import kenlm

path = 'path/to/model.arpa'
lm = kenlm.LanguageModel(path)

sentence = &quot;I am not superstitious but I am a little stitious&quot;
print(model.score(sentence))
</code></pre>
<p>Which prints something like</p>
<pre><code>----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
****************************************************************************************************
-24.47921371459961
</code></pre>
<p>Now if you&rsquo;re interested in a bit more information about what&rsquo;s going on, you can add this bit at the bottom:</p>
<pre><code class="language-python">words = ['&lt;s&gt;'] + sentence.split() + ['&lt;/s&gt;']
for i, (prob, length, oov) in enumerate(lm.full_scores(sentence)):
  print(f'{prob} {length}: {&quot; &quot;.join(words[i+2-length:i+2])}')
  if oov:
    print(f'\t&quot;{words[i+1]}&quot; is an OOV')

for w in words:
  if not w in lm:
    print(f'&quot;{w}&quot; is an OOV')
</code></pre>
<p>Which adds this to your output:</p>
<pre><code>-3.1138248443603516 2: &lt;s&gt; I
-1.1560251712799072 3: &lt;s&gt; I am
-1.1645264625549316 3: I am not
-4.912360191345215 1: superstitious
-4.504511833190918 1: but
-2.2214112281799316 2: but I
-1.1531075239181519 3: but I am
-1.2614283561706543 3: I am a
-0.9001830816268921 3: am a little
-1.2325057983398438 3: a little stitious
	&quot;stitious&quot; is an OOV
-2.8593297004699707 2: stitious &lt;/s&gt;
&quot;stitious&quot; is an OOV
</code></pre>
<p>To the left of each term is the log (base 10) probability of each term occurring. For the first term, <code>&lt;s&gt; I</code> means start of sentence followed by &ldquo;I&rdquo;, which the model has assigned a log probability of -3.11. That&rsquo;s around 0.00078. You might think it&rsquo;s strange that a sentence beginning with &ldquo;I&rdquo; is so unlikely but we are using Wikitext-2. Wikitext-2 is Wikipedia articles. Not a lot of sentences on Wikipedia begin with &ldquo;I&rdquo;.</p>
<p>Notice that &ldquo;stitious&rdquo; is an OOV (out of vocabulary) term here. Clearly the language model doesn&rsquo;t appreciate humor. We&rsquo;ll have to tackle that next time.</p>
]]></content>
        </item>
        
        <item>
            <title>What is Temperature in NLP?🐭</title>
            <link>https://blog.lukesalamone.com/posts/what-is-temperature/</link>
            <pubDate>Fri, 02 Apr 2021 00:50:38 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/what-is-temperature/</guid>
            <description>&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    // Fix &lt;code&gt; tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;p&gt;Temperature is a parameter used in natural language processing models to increase or decrease the &amp;ldquo;confidence&amp;rdquo; a model has in its most likely response.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    // Fix <code> tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<p>Temperature is a parameter used in natural language processing models to increase or decrease the &ldquo;confidence&rdquo; a model has in its most likely response.</p>
<p>In my opinion, the most intuitive way of understanding how temperature affects model outputs is to play with it yourself. If you&rsquo;re interested in the mathematical details, I&rsquo;ve included them below, but I won&rsquo;t be offended if you just want to play around with the slider 😃 .</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>
<script src="/js/temperature-demo.js"></script>
<p><canvas id="myChart" style="background-color: #0000"></canvas>
<span>Temperature (θ):
<input type="range" min="1" max="1000" value="250" class="slider" id="myRange">
<span id="temperature">25.0</span></p>
<h2 id="whats-going-on">What&rsquo;s going on?</h2>
<p>Suppose we have a language model which predicts the last word in the sentence &ldquo;The mouse ate the _____&rdquo;. Given the previous words in the sentence and its prior training, our language model will try to fill in the blank with a reasonable final token. Suppose those raw outputs are as follows:</p>
<table>
	<thead>
			<tr>
					<th>token</th>
					<th>logit</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>cat</td>
					<td>3</td>
			</tr>
			<tr>
					<td>cheese</td>
					<td>70</td>
			</tr>
			<tr>
					<td>pizza</td>
					<td>40</td>
			</tr>
			<tr>
					<td>cookie</td>
					<td>65</td>
			</tr>
			<tr>
					<td>fondue</td>
					<td>55</td>
			</tr>
			<tr>
					<td>banana</td>
					<td>10</td>
			</tr>
			<tr>
					<td>baguette</td>
					<td>15</td>
			</tr>
			<tr>
					<td>cake</td>
					<td>12</td>
			</tr>
	</tbody>
</table>
<p>These outputs make sense. A mouse probably eats cheese, but <a href="https://en.wikipedia.org/wiki/If_You_Give_a_Mouse_a_Cookie">mice are also known to eat cookies</a>. A mouse probably wouldn&rsquo;t eat a baguette unless it was <a href="https://imgur.com/a/484AEO3">a French mouse</a>.</p>
<p>Since these are the raw outputs of the model, they won&rsquo;t sum to 100. To normalize these values, we typically use <a href="https://en.wikipedia.org/wiki/Softmax_function">softmax</a>:</p>
<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<p style="text-align: center; font-size: 1.5em">
$ σ(z_i) = {e^{z_i} \over \sum_{j=0}^N e^{z_j}} $
</p>
<p>When modulating with temperature, we introduce an additional temperature variable θ which affects the softmax distribution. A higher temperature θ &ldquo;excites&rdquo; previously low probability outputs. A lower temperature θ lowers the smaller outputs relative to the largest outputs. To accomplish this, we replace each z<sub>i</sub> in the formula above with the quotient z<sub>i</sub>/θ:</p>
<p style="text-align: center; font-size: 1.5em">
$ σ(z_i) = {e^{z_i \over θ} \over \sum_{j=0}^N e^{z_j \over θ}} $
</p>
<p>Higher temperatures make the model more &ldquo;creative&rdquo; which can be useful when generating prose, for example. Lower temperatures make the model more &ldquo;confident&rdquo; which can be useful in applications like question answering.</p>
]]></content>
        </item>
        
        <item>
            <title>What is Perplexity?</title>
            <link>https://blog.lukesalamone.com/posts/perplexity/</link>
            <pubDate>Thu, 01 Apr 2021 12:14:49 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/perplexity/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;p&gt;&lt;strong&gt;TLDR: NLP metric ranging from 1 to infinity. Lower is better.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In natural language processing, perplexity is the most common metric used to measure the performance of a language model. To calculate perplexity, we use the following formula:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<p><strong>TLDR: NLP metric ranging from 1 to infinity. Lower is better.</strong></p>
<p>In natural language processing, perplexity is the most common metric used to measure the performance of a language model. To calculate perplexity, we use the following formula:</p>
<p style="text-align: center; font-size: 1.5em">
$ perplexity = e^z $
</p>
<p style="text-align:center">where</p>
<p style="text-align: center; font-size: 1.5em">
$ z = -{1 \over N} \sum_{i=0}^N ln(P_{n}) $
</p>
<p>Typically we use base <code>e</code> when calculating perplexity, but this is not required. Any base will do, so sometimes the formula will use base 2 or base 10, along with logarithms to the corresponding base.</p>
<h2 id="example">Example</h2>
<p>Imagine that we have a language model which generates the following sequence of tokens:</p>
<p><code>&lt;start&gt;</code> <code>jack</code> <code>and</code> <code>jill</code> <code>went</code> <code>up</code> <code>the</code> <code>hill</code></p>
<p>And suppose that the conditional probabilities for each of the tokens are as follows:</p>
<table>
	<thead>
			<tr>
					<th>token</th>
					<th>probability</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>&lt;start&gt;</code></td>
					<td>15%</td>
			</tr>
			<tr>
					<td><code>jack</code></td>
					<td>5%</td>
			</tr>
			<tr>
					<td><code>and</code></td>
					<td>12%</td>
			</tr>
			<tr>
					<td><code>jill</code></td>
					<td>18%</td>
			</tr>
			<tr>
					<td><code>went</code></td>
					<td>25%</td>
			</tr>
			<tr>
					<td><code>up</code></td>
					<td>40%</td>
			</tr>
			<tr>
					<td><code>the</code></td>
					<td>33%</td>
			</tr>
			<tr>
					<td><code>hill</code></td>
					<td>50%</td>
			</tr>
	</tbody>
</table>
<p>For the purposes of calculating perplexity it doesn&rsquo;t matter how the sequence was generated. It may be using an n-gram model or an LSTM or a transformer. All that matters is the probabilities the model assigns to each of the tokens. To calculate perplexity, we calculate the logarithm of each of the values above:</p>
<table>
	<thead>
			<tr>
					<th>token</th>
					<th>P</th>
					<th>ln(P)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>&lt;start&gt;</code></td>
					<td>15%</td>
					<td>-1.897</td>
			</tr>
			<tr>
					<td><code>jack</code></td>
					<td>5%</td>
					<td>-2.996</td>
			</tr>
			<tr>
					<td><code>and</code></td>
					<td>12%</td>
					<td>-2.120</td>
			</tr>
			<tr>
					<td><code>jill</code></td>
					<td>18%</td>
					<td>-1.715</td>
			</tr>
			<tr>
					<td><code>went</code></td>
					<td>25%</td>
					<td>-1.386</td>
			</tr>
			<tr>
					<td><code>up</code></td>
					<td>40%</td>
					<td>-0.916</td>
			</tr>
			<tr>
					<td><code>the</code></td>
					<td>33%</td>
					<td>-1.109</td>
			</tr>
			<tr>
					<td><code>hill</code></td>
					<td>50%</td>
					<td>-0.693</td>
			</tr>
	</tbody>
</table>
<p>Summing the logs, we get -12.832. Since there are 8 tokens, we divide -12.832 by 8 to get -1.604. Negating that allows us to calculate the final perplexity:</p>
<p style="text-align: center; font-size: 1.5em">
$ perplexity = e^{1.604} = 4.973 $
</p>
<p>Therefore the perplexity of this sequence is about 4.973.</p>
]]></content>
        </item>
        
        <item>
            <title>S3 Bucket Url</title>
            <link>https://blog.lukesalamone.com/posts/s3-bucket-url/</link>
            <pubDate>Wed, 10 Mar 2021 03:03:53 -0600</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/s3-bucket-url/</guid>
            <description>&lt;p&gt;Assuming your bucket is publicly accessible, the url of your S3 bucket will be&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://[bucket-name].s3-website-[region].amazonaws.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example for &amp;ldquo;mybucket&amp;rdquo; in &amp;ldquo;us-east-1&amp;rdquo; your url will be&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://mybucket.s3-website-us-east-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;</description>
            <content type="html"><![CDATA[<p>Assuming your bucket is publicly accessible, the url of your S3 bucket will be</p>
<pre><code>http://[bucket-name].s3-website-[region].amazonaws.com
</code></pre>
<p>For example for &ldquo;mybucket&rdquo; in &ldquo;us-east-1&rdquo; your url will be</p>
<pre><code>http://mybucket.s3-website-us-east-1.amazonaws.com
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>RL Trading Agent</title>
            <link>https://blog.lukesalamone.com/posts/rl-trading-agent/</link>
            <pubDate>Mon, 08 Mar 2021 20:11:28 -0600</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/rl-trading-agent/</guid>
            <description>&lt;p&gt;Here we will demonstrate an implementation of the paper &lt;a href=&#34;https://www.sciencedirect.com/science/article/abs/pii/S0957417418306134&#34;&gt;Improving financial trading decision using deep Q-learning: Predicting the number of shares, action strategies, and transfer learning&lt;/a&gt; by Jeong et al. This paper covers three separate deep Q-learning architectures, transfer learning, two different means of index component rankings, and action strategies for dealing with confused markets.&lt;/p&gt;
&lt;p&gt;Trading agents for finance are nothing new. Previous attempts at creating automated trading systems have used statistical indicators such as moving average to determine how to act at any time. However, most of these agents focus on the action to take, opting to trade a fixed number of shares. This is not realistic for real-world trading scenarios.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Here we will demonstrate an implementation of the paper <a href="https://www.sciencedirect.com/science/article/abs/pii/S0957417418306134">Improving financial trading decision using deep Q-learning: Predicting the number of shares, action strategies, and transfer learning</a> by Jeong et al. This paper covers three separate deep Q-learning architectures, transfer learning, two different means of index component rankings, and action strategies for dealing with confused markets.</p>
<p>Trading agents for finance are nothing new. Previous attempts at creating automated trading systems have used statistical indicators such as moving average to determine how to act at any time. However, most of these agents focus on the action to take, opting to trade a fixed number of shares. This is not realistic for real-world trading scenarios.</p>
<p>This repository contains all of the code you&rsquo;ll need to train and run a deep-Q agent, but if you&rsquo;d like a deeper understanding of how it all works we&rsquo;ve included a tutorial below.</p>
<h1 id="tutorial-overview">Tutorial Overview</h1>
<p>After following this tutorial you will have completed the following:</p>
<ul>
<li>Scraped four different stock indices for daily stock data trained</li>
<li>Trained three deep-Q trading architectures:
<ul>
<li>NumQ, a simple feed-forward network which uses a single branch to determine both the action and number of shares to trade.</li>
<li>NumDReg-AD, an action-dependent network with a branch controlling the action to take and another branch controlling the number of shares to trade.</li>
<li>NumDReg-ID, an action-independent network with two branches as above, but with the number of shares entirely independent of the action taken.</li>
</ul>
</li>
<li>Scraped all components of the four stock indices used above and classified the components using two different methodologies.</li>
<li>Trained an autoencoder, creating a neural basis for stock similarity within an index.</li>
<li>Pretrained multiple models on groups of component stocks within each index.</li>
<li>Trained final models on index data.</li>
<li>Drawn beautiful charts displaying your results.</li>
</ul>
<p>This tutorial assumes a basic understanding of Python and Pytorch. If you would like to brush up on Pytorch we highly recommend their <a href="https://pytorch.org/tutorials/">tutorials</a>.</p>
<h1 id="scraping-stock-indices">Scraping stock indices</h1>
<p>This repository contains a file called <code>download_stock_data.py</code>. Edit this file to include the names of the components for each of the following stock indices:</p>
<ul>
<li>Dow Jones Industrial Average (<code>^DJI</code>)</li>
<li>S&amp;P 500 (<code>^GSPC</code>)</li>
<li>NASDAQ (<code>^IXIC</code>)</li>
<li>New York Stock Exchange (<code>^NYSE</code>)</li>
</ul>
<pre><code class="language-python">INDEX_NAME = 'ixic'
COMPONENTS_LIST = ['OPTT', 'NMRK', 'SCOA', ...]
</code></pre>
<p>Running this file will save each of the components into a directory <code>raw/{index_name}/</code> with each component saved as a CSV file.</p>
<h1 id="numq">NumQ</h1>
<p>The Jeong paper experiments with three architectures for trading. The first and simplist of these architectures is called NumQ which uses a single branch of fully-connected layers to determine both the action to take and the ratios for those actions. Its structure is shown below:</p>
<p><img src="src/img/numq.png" alt="numq architecture"></p>
<p>This can be succinctly represented in pytorch as a series of <code>torch.nn.Linear</code> layers:</p>
<pre><code class="language-python">class NumQModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(in_features=200, out_features=200, bias=True)
        self.fc2 = nn.Linear(in_features=200, out_features=100, bias=True)
        self.fc3 = nn.Linear(in_features=100, out_features=50, bias=True)
        self.fc_q = nn.Linear(in_features=50, out_features=3, bias=True)

    def forward(self, x: Tensor) -&gt; Tuple[Tensor, Tensor]:
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)

        q = self.fc_q(F.relu(x))
        r = F.softmax(self.fc_q(torch.sigmoid(x)))

        return q, r

</code></pre>
<p>Of note in this model are the return values of the <code>forward()</code> function. Rather than simply returning one result of a forward-pass through the network, the NumQ network will return two values: a list of Q values associated with the input, and the ratios for each of the three actions (BUY, HOLD, SELL). When in use, the Q values determine which action will be taken, while the ratios determine with how many shares the action will be executed with.</p>
<h1 id="numdreg-ad">NumDReg-AD</h1>
<p>The second model is a bit more complex. It contains a branch for determining the Q values associated with the input, as well as a separate branch for determining the numbers of shares to trade.</p>
<p><img src="src/img/numdregad.png" alt="numdreg-ad architecture"></p>
<p>This architecture is quite a bit more involved than NumQ. In fact, this increase in complexity motivates a reduction in each of the fully-connected layers in the network from those in NumQ: 100, 50, 20 versus the 200, 100, 50 used in NumQ. In spite of this adjustment, this and the next model, NumDReg-ID, need to be trained using a three-step training process. The training process will be discussed later on.</p>
<pre><code class="language-python">class NumDRegModel(nn.Module):
    def __init__(self, method):
        super().__init__()

        # Set method
        self.method = method

        # Training step
        self.step = 0

        # root
        self.fc1 = nn.Linear(in_features=200, out_features=100, bias=True)

        # action branch
        self.fc2_act = nn.Linear(in_features=100, out_features=50, bias=True)
        self.fc3_act = nn.Linear(in_features=50, out_features=20, bias=True)
        self.fc_q = nn.Linear(in_features=20, out_features=3, bias=True)

        # number branch
        self.fc2_num = nn.Linear(in_features=100, out_features=50, bias=True)
        self.fc3_num = nn.Linear(in_features=50, out_features=20, bias=True)
        self.fc_r = nn.Linear(in_features=20, out_features=(3 if self.method == NUMDREG_AD else 1), bias=True)

    def forward(self, x: Tensor) -&gt; Tuple[Tensor, Tensor]:
        # Root
        x = F.relu(self.fc1(x))

        # Action branch
        x_act = F.relu(self.fc2_act(x))
        x_act = F.relu(self.fc3_act(x_act))
        q = self.fc_q(x_act)

        if self.step == 1:
            # Number branch based on q values
            r = F.softmax(self.fc_q(torch.sigmoid(x_act)))
        else:
            # Number branch
            x_num = F.relu(self.fc2_num(x))
            x_num = torch.sigmoid(self.fc3_num(x_num))
            # Output layer depends on method
            if self.method == NUMDREG_ID:
                r = torch.sigmoid(self.fc_r(x_num))
            else:
                r = F.softmax(self.fc_r(x_num))

        return q, r

    def set_step(self, s):
        self.step = s
</code></pre>
<h1 id="numdreg-id">NumDReg-ID</h1>
<p>This is the third and final paper introduced in the paper. It contains an action-independent branch which specifies the number of shares to trade. Its architecture is identical to NumDReg-AD except for the output layer in the number branch which has a size of one. The activation function of the number branch was not specified in the paper, so we opted to use the <a href="https://en.wikipedia.org/wiki/Sigmoid_function">sigmoid function</a>.</p>
<p>(Sigmoid has the convenient property of being bounded between 0 and 1. This avoids the awkward circumstance where the action branch predicts BUY while the number branch predicts a negative number, resulting in a SELL action.)</p>
<p><img src="src/img/numdregid.png" alt="numdreg-id architecture"></p>
<h1 id="reinforcement-learning">Reinforcement learning</h1>
<p>At each timestep t, the agent observes a state of 200 price differences, chooses an action (either <code>BUY</code>, <code>HOLD</code>, or <code>SELL</code>) with a number of shares, and receives a reward equal to the normalized profit.</p>
<h1 id="training-process">Training process</h1>
<p>Reinforcement learning agents are trained over a number of episodes, during which they observe states, take actions, receive rewards, and observe the next state. These (state, action, reward, next_state) transitions are stored in a memory buffer which the agent then uses to optimize its neural network.</p>
<p>The learning process for deep Q networks is a bit different from normal Q learning models. Deep Q models typically contain two neural networks working in tandem: a policy network which evaluates a given state, and a target network which is periodically updated with the weights from the policy net. This periodic update pattern helps to maintain stability while training.</p>
<p>Our training logic defines an episode as one chronological pass through the training data. This detail is not specified in the paper, but one pass over the data makes logical sense in this context. We used a <code>TradingEnvironment</code> class to track information during training, which has the added benefit of making the code more readable. The details of <code>TradingEnvironment</code> will be discussed later on.</p>
<p>At the beginning of training, the policy network and target network are initialized. After this, we begin to iterate over a number of episodes.</p>
<p>During an episode, the agent is provided with a state, which you will recall is a number of price differences. This state is fed into the policy network, which will calculate Q values and ratios. We then calculate the reward for each of the three actions. The (state, action, reward, next_state) transitions for each of the actions are stored in a memory buffer. Next, the model will undergo an optimization step.</p>
<p>During optimization, batch transitions are retrieved from the memory buffer. Then the loss is computed as the difference between actual and expected Q values and backpropagated through the policy net.</p>
<p>After an episode concludes, we do an update of the target network with the policy network and reset the environment to begin serving states from the beginning of the episode again.</p>
<h1 id="3-step-training-numdreg-adid">3 step training (NumDReg-AD/ID)</h1>
<ol>
<li>Train the shared root and action branch only. The number branch is frozen.</li>
<li>Train the number branch from the action branch&rsquo;s Q-values. The action branch is frozen.</li>
<li>Train the entire network end-to-end.</li>
</ol>
<p>Each step runs for the same number of episodes.</p>
<h1 id="component-stock-relationships">Component stock relationships</h1>
<p>Because of their large number of weights, deep neural networks have the tendency to overfit their training data if there isn&rsquo;t enough of it. To counteract this behavior, we reduced the number of weights and changed the training process into a 3-step procedure. One other thing which can help to prevent overfitting is to train with more data. This is why we pretrain using component stocks and finish training on the index stock.</p>
<p>But first we need to choose which components to pretrain with. Training with all of them will be too time-consuming. Instead, we will create 6 groups of stocks based on 2 different measures of the components: <a href="https://en.wikipedia.org/wiki/Pearson_correlation_coefficient">Pearson correlation</a> and the mean squared predicting error from an autoencoder. The autoencoder implementation will be discussed in the next section. We will measure each component stock and choose 6 groups according to the following:</p>
<table>
	<thead>
			<tr>
					<th>Correlation</th>
					<th>MSE</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Highest 2n</td>
					<td>Highest 2n</td>
			</tr>
			<tr>
					<td>Lowest 2n</td>
					<td>Lowest 2n</td>
			</tr>
			<tr>
					<td>Highest n + Lowest n</td>
					<td>Highest n + Lowest n</td>
			</tr>
	</tbody>
</table>
<p>So for each stock we will measure its correlation with the index and measure its autoencoder MSE. One group of stocks will be made up of the highest <code>2n</code> components when measured by correlation with the index. (Here <code>n</code> is chosen in proportion to the number of components in the index.) Another group will be made up of the <code>2n</code> stocks which have the lowest correlation with the index. And so forth.</p>
<p>Creating these groups allows our models to be pretrained on stocks which are proxies to the index, or in the case of the &ldquo;low&rdquo; group, on stocks which might help the agent generalize later on.</p>
<h1 id="autoencoding-component-stocks">Autoencoding component stocks</h1>
<p>In order to calculate the mean squared error of the component stocks, we need to train an autoencoder which will predict a series of stock prices for each component in an index. That is, the input size of the network will be MxN, where M is the number of components in the index and N is the number of days in the time series.</p>
<p>We will train an autoencoder such that X=Y, where X is the input and Y is the output. The architecture of the autoencoder is very simple, having only 2 hidden layers with 5 units each. These small hidden layers force the model to encode the most essential information of its inputs into a small latent space. All extraneous information not represented in the latent space is discarded. Each of the inputs x<sub>i</sub> will be encoder with the autoencoder as y<sub>i</sub>, and it is against these that mean squared error is measured.</p>
<p><img src="/src/img/autoencoder.png" alt="autoencoder"></p>
<p>Training an autoencoder is fairly simple if you understand the basics of neural networks. Rather than training with (input, target) pairs, since we only care that input=input, we will train on (input, input) pairs.</p>
<h1 id="transfer-learning">Transfer learning</h1>
<p>Financial datasets are small, so we pretrain on grouped component stocks, then transfer those weights to train on the actual index. Stock groupings are chosen by two measures — Pearson correlation with the index and autoencoder reconstruction MSE — each producing a &ldquo;high 2n,&rdquo; &ldquo;low 2n,&rdquo; and &ldquo;high n + low n&rdquo; group.</p>
<p>When the max Q-value for an action is within 0.0002 of the min, the market signal is too weak to act on, so the agent defaults to <code>HOLD</code>.</p>
<h1 id="finance-environment">Finance Environment</h1>
<p>For convenience, we include an environment for training which encapsulates many variables that would otherwise need to be tracked in the training loop. Instead, we use the <code>FinanceEnvironment</code> to store these variables and provide them as needed to the agent during training.</p>
<p>The <code>FinanceEnvironment</code> class exposes only a few necessary methods to the training loop. For example, <code>step()</code> returns <code>state</code> and <code>done</code>. The first of these is the difference in stock prices for the 200 days prior to the current day. The second indicates whether the episode is done. Executing <code>step()</code> updates several internal variables used in profit and reward calculations.</p>
<p>The other important method of the environment is <code>update_replay_memory()</code>. This method adds a <code>(state, action, rewards_all_actions, next_state)</code> transition to the replay memory. This will be sampled later when the model is optimized. Because the environment stores and updates most of these variables internally, they do not clutter up the training loop.</p>
<h1 id="putting-it-all-together">Putting it all together</h1>
<p>You can see our implementation in the <a href="https://github.com/lukesalamone/deep-q-trading-agent">github repo</a>.</p>
<pre><code class="language-bash">python main.py --train --architecture numdreg_ad --index ixic
</code></pre>
<p>See <code>python main.py --help</code> for all options: architecture selection, pretraining, tensorboard logging, and checkpointing.</p>
<h1 id="selected-results">Selected Results</h1>
<p>NumQ and NumDReg-ID generally outperformed NumDReg-AD across indices, with the best returns on ^GSPC (S&amp;P 500). Final cumulative profits fell short of the paper&rsquo;s reported figures — likely due to unreported hyperparameters, a shorter training window, and the inherent difficulty of equity prediction. Full result tables and Sharpe ratio comparisons are in the <a href="https://github.com/lukesalamone/deep-q-trading-agent">repo</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>About My Quick Reference Articles</title>
            <link>https://blog.lukesalamone.com/posts/why-how-to/</link>
            <pubDate>Sun, 07 Mar 2021 14:44:37 -0600</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/why-how-to/</guid>
            <description>&lt;p&gt;I&amp;rsquo;ve created a few quick-reference articles and it might not be clear why. There are a few reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;These articles are mainly a reference for me. I find myself searching the same things over and over, looking for the purple link, scrolling through the article, then copy &amp;amp; pasting code. I&amp;rsquo;d rather not go through the hassle. These articles aim to solve that problem.&lt;/li&gt;
&lt;li&gt;I aim to keep the answers &lt;a href=&#34;https://en.wikipedia.org/wiki/Above_the_fold#In_web_design&#34;&gt;above the fold&lt;/a&gt;. I don&amp;rsquo;t want to have to scroll down to find the answer. I almost never read the surrounding prose when I am in &amp;ldquo;coding mode&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;I don&amp;rsquo;t have ads or popups on my blog. I will never ask people to sign up for a newsletter or login to read more. I also don&amp;rsquo;t use pictures unless there&amp;rsquo;s a good reason. &lt;a href=&#34;https://www.google.com/search?q=ai&amp;#43;thinking&amp;#43;robot&amp;#43;stock&amp;#43;photo&amp;amp;tbm=isch&#34;&gt;The &amp;ldquo;thinking AI robot stock photo&amp;rdquo; industry is definitely a bubble.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Writing these things out explicitly helps me to remember them. Paradoxically, this may make these how-to pages less useful to me, but maybe someone else will find them useful.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These quick reference articles don&amp;rsquo;t explain much because I don&amp;rsquo;t need an explanation of what is going on. I just need a chunk of working code. There are other websites which have far more comprehensive guides covering how to do things. They cover all of the fundamentals of how things are done. But I don&amp;rsquo;t need that, I just want a 30 second reference with a working chunk of code.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>I&rsquo;ve created a few quick-reference articles and it might not be clear why. There are a few reasons:</p>
<ol>
<li>These articles are mainly a reference for me. I find myself searching the same things over and over, looking for the purple link, scrolling through the article, then copy &amp; pasting code. I&rsquo;d rather not go through the hassle. These articles aim to solve that problem.</li>
<li>I aim to keep the answers <a href="https://en.wikipedia.org/wiki/Above_the_fold#In_web_design">above the fold</a>. I don&rsquo;t want to have to scroll down to find the answer. I almost never read the surrounding prose when I am in &ldquo;coding mode&rdquo;.</li>
<li>I don&rsquo;t have ads or popups on my blog. I will never ask people to sign up for a newsletter or login to read more. I also don&rsquo;t use pictures unless there&rsquo;s a good reason. <a href="https://www.google.com/search?q=ai&#43;thinking&#43;robot&#43;stock&#43;photo&amp;tbm=isch">The &ldquo;thinking AI robot stock photo&rdquo; industry is definitely a bubble.</a></li>
<li>Writing these things out explicitly helps me to remember them. Paradoxically, this may make these how-to pages less useful to me, but maybe someone else will find them useful.</li>
</ol>
<p>These quick reference articles don&rsquo;t explain much because I don&rsquo;t need an explanation of what is going on. I just need a chunk of working code. There are other websites which have far more comprehensive guides covering how to do things. They cover all of the fundamentals of how things are done. But I don&rsquo;t need that, I just want a 30 second reference with a working chunk of code.</p>
<h1 id="articles">Articles</h1>
<ul>
<li><a href="/posts/python-format-string/">Python: Formatting a string</a></li>
<li><a href="/posts/read-write-json/">Python: Read &amp; Write Json</a></li>
<li><a href="/posts/python-serve-html/">Python: Serve an HTML File</a></li>
<li><a href="/posts/managing-python-environments/">Managing Python Environments</a></li>
<li><a href="/posts/connect-to-colab/">Colab: Connect to Google Drive</a></li>
<li><a href="/posts/connect-jupyter-to-remote/">Connect Jupyter to Remote</a></li>
<li><a href="/posts/s3-bucket-url/">S3 Bucket Url</a></li>
<li><a href="/posts/how-to-tar-untar-file/">How to Zip and Unzip a tar.gz File</a></li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>Python: Read &amp; Write Json</title>
            <link>https://blog.lukesalamone.com/posts/read-write-json/</link>
            <pubDate>Sun, 07 Mar 2021 14:05:27 -0600</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/read-write-json/</guid>
            <description>&lt;p&gt;Often it is useful to save python data to json files. The following code will demonstrate how that can be done.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;God bless JSON!&amp;rdquo; ~ a soon to be famous programmer&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;import json

data = {&#39;a&#39;: 1, &#39;b&#39;:&#39;hello&#39;, &#39;c&#39;:False}
filename = &#39;awesome_data.json&#39;

# write data to file
with open(filename, &#39;w&#39;) as f:
  json.dump(data, f)


# read json from file
with open(filename, &#39;r&#39;) as f:
  data = json.load(f)


print(data)
# prints {&#39;a&#39;: 1, &#39;b&#39;:&#39;hello&#39;, &#39;c&#39;:False}
&lt;/code&gt;&lt;/pre&gt;</description>
            <content type="html"><![CDATA[<p>Often it is useful to save python data to json files. The following code will demonstrate how that can be done.</p>
<blockquote>
<p>&ldquo;God bless JSON!&rdquo; ~ a soon to be famous programmer</p>
</blockquote>
<pre><code class="language-python">import json

data = {'a': 1, 'b':'hello', 'c':False}
filename = 'awesome_data.json'

# write data to file
with open(filename, 'w') as f:
  json.dump(data, f)


# read json from file
with open(filename, 'r') as f:
  data = json.load(f)


print(data)
# prints {'a': 1, 'b':'hello', 'c':False}
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>Autoencoding Stock Prices</title>
            <link>https://blog.lukesalamone.com/posts/build-an-autoencoder/</link>
            <pubDate>Sun, 07 Mar 2021 01:31:51 -0600</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/build-an-autoencoder/</guid>
            <description>&lt;figure&gt;&lt;img src=&#34;../img/autoencoder.png&#34;
			alt=&#34;Autoencoding stock prices as found in Heaton et al., 2016&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;Autoencoding stock prices as found in Heaton et al., 2016&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;So you want to build an autoencoder? Great! This article will demonstrate how to build an autoencoder and use it to measure stock prices against an index. This technique is described in more technical terms &lt;a href=&#34;https://arxiv.org/pdf/1602.06561.pdf&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once we&amp;rsquo;ve trained the autoencoder, we can use it to measure how well each component follows the other members of the index. This can be useful for finding deeper insights into an index, and doesn&amp;rsquo;t require a priori knowledge of the index price or the weighting of its components. Note, this is only one metric which one could use to determine how well one member of the group follows the group overall. Another might be &lt;a href=&#34;https://en.wikipedia.org/wiki/Pearson_correlation_coefficient&#34;&gt;Pearson Correlation&lt;/a&gt;.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<figure><img src="/img/autoencoder.png"
			alt="Autoencoding stock prices as found in Heaton et al., 2016"><figcaption>
			<p>Autoencoding stock prices as found in Heaton et al., 2016</p>
		</figcaption>
</figure>

<p>So you want to build an autoencoder? Great! This article will demonstrate how to build an autoencoder and use it to measure stock prices against an index. This technique is described in more technical terms <a href="https://arxiv.org/pdf/1602.06561.pdf">here</a>.</p>
<p>Once we&rsquo;ve trained the autoencoder, we can use it to measure how well each component follows the other members of the index. This can be useful for finding deeper insights into an index, and doesn&rsquo;t require a priori knowledge of the index price or the weighting of its components. Note, this is only one metric which one could use to determine how well one member of the group follows the group overall. Another might be <a href="https://en.wikipedia.org/wiki/Pearson_correlation_coefficient">Pearson Correlation</a>.</p>
<h2 id="github-repository">Github repository</h2>
<p>To follow along with the code in this tutorial, please download the <a href="https://github.com/lukesalamone/stock-data-autoencoder">corresponding repository on Github</a>:</p>
<pre><code>git clone git@github.com:lukesalamone/stock-data-autoencoder.git
cd stock-data-autoencoder
pip install -r requirements.txt
</code></pre>
<h1 id="what-is-an-autoencoder">What is an autoencoder?</h1>
<p>An autoencoder is a neural network which encodes information back to itself. The structure of the network is such that the input layers (the &ldquo;encoder&rdquo;) will be large compared to the hidden layers (the &ldquo;code&rdquo;), forcing the network to compress information inside its hidden layers.</p>
<p>The idea of our autoencoder is that we would like to encode stock price information back to itself while discarding trends that aren&rsquo;t important. To do this, we will feed our network stock price data and ask the network to return those prices to us as outputs. Component stocks which are important to the index will be preserved well and thus be highly accurate, while components which are less important will not be well-preserved. We will measure the performance of the network on each component using mean squared error.</p>
<h1 id="the-model">The model</h1>
<p>We will use an autoencoder with a number of inputs and outputs equal to the number of component stocks in our index. For this exercise, we will use the <a href="https://en.wikipedia.org/wiki/S%26P_500">S&amp;P 500 index</a> which contains 505 components. This means our input and output size will be 505. We will also use a hidden layer with 5 units.</p>
<pre><code class="language-python">class StonksNet(nn.Module):
 def __init__(self, size):
   super().__init__()
     self.fc1 = nn.Linear(in_features=size, out_features=5)
     self.out = nn.Linear(in_features=5, out_features=size)

 def forward(self, x: Tensor) -&gt; Tensor:
   x = F.relu(self.fc1(x))
   x = F.relu(self.out(x))
   return x
</code></pre>
<h1 id="the-data">The data</h1>
<p>We will use daily stock prices downloaded using <a href="https://pypi.org/project/yfinance/">yfinance</a>. This data is readily available online and I recommend downloading it for yourself. We will use data between January 1, 1991 to January 1, 2021 (30 years of data).</p>
<p>To download the S&amp;P 500 stock data please run <code>gather_stocks.py</code> from the project directory:</p>
<pre><code>python gather_stocks.py
</code></pre>
<p>This will download all 505 components into the <code>stock_data</code> directory. Data will also be cleaned such that each component has the same number of days, which will be important when feeding it into the model.</p>
<h1 id="training-the-model">Training the model</h1>
<p>The model itself is a simple feed-forward neural network. As such, we use a standard training loop to train the model. We don&rsquo;t expect the loss to ever fall to zero during training since it is impossible for the network to perfectly encode and decode so many inputs into so few hidden code units. Some information will inevitably be lost. In my training, validation losses bottomed out at around 4000, but yours may be different depending on the initialization of your autoencoder.</p>
<p><img src="/img/autoencoder_validation_losses.png" alt="validation loss"></p>
<h1 id="ranking-components">Ranking components</h1>
<p>Finally we&rsquo;re ready to rank the components of the S&amp;P 500 for &ldquo;closeness&rdquo;. After running <code>python train_model.py</code> you will see the best and worst components as scored by the autoencoder. Here were my results, yours may be different.</p>
<pre><code>best 5 results:
DRE: 16.66
LNT: 37.27
MU: 38.88
HOLX: 43.18
CERN: 47.46

worst 5 results:
HUM: 105244.19
SHW: 108542.73
LMT: 113654.48
C: 357073.88
NVR: 10955169.00
</code></pre>
<h1 id="future-research">Future research</h1>
<p>Upon inspection, it appears that better results might be achieved if we normalize the stock data before training. It appears that stocks with higher prices and higher volatility tended to perform worse than those with tight price ranges. In a way this is expected, since the autoencoder will naturally have a harder time modeling large values with a limited set of hidden units. However, normalizing the prices into similar ranges might be an interesting exercise to see if we can squeeze even more out of the model.</p>
]]></content>
        </item>
        
        <item>
            <title>Python: Formatting a string</title>
            <link>https://blog.lukesalamone.com/posts/python-format-string/</link>
            <pubDate>Wed, 24 Feb 2021 21:22:42 -0600</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/python-format-string/</guid>
            <description>&lt;p&gt;There are three main ways to format strings in python:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;name = &#39;Luke&#39;
food = &#39;pizza&#39;

# old style
&amp;quot;My name is %s and I like %s.&amp;quot; % (name, food)

# str.format()
&amp;quot;My name is {0} and I like {1}.&amp;quot;.format(name, food)

# f-strings
f&amp;quot;My name is {name} and I like {food}.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;</description>
            <content type="html"><![CDATA[<p>There are three main ways to format strings in python:</p>
<pre><code class="language-python">name = 'Luke'
food = 'pizza'

# old style
&quot;My name is %s and I like %s.&quot; % (name, food)

# str.format()
&quot;My name is {0} and I like {1}.&quot;.format(name, food)

# f-strings
f&quot;My name is {name} and I like {food}.&quot;
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>Siamese Neural Networks (Video)</title>
            <link>https://blog.lukesalamone.com/posts/siamese-nn-video/</link>
            <pubDate>Thu, 17 Dec 2020 11:22:43 -0600</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/siamese-nn-video/</guid>
            <description>&lt;div style=&#34;text-align:center&#34;&gt;
  &lt;iframe src=&#34;https://player.vimeo.com/video/491725663&#34; width=&#34;640&#34; height=&#34;360&#34; frameborder=&#34;0&#34; allow=&#34;autoplay; fullscreen&#34; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;em&gt;The following is a transcript of the above video&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In this paper, the authors present a novel neural network architecture to enable audio search via sounds humans are able to make, for example humming and whistling. This is an important capability when searching through audio for a specific sound.&lt;/p&gt;
&lt;h2 id=&#34;motivation&#34;&gt;Motivation&lt;/h2&gt;
&lt;p&gt;Imagine you have hundreds of unlabeled sound effects on your computer, and you are looking for a specific one. It could be very tedious to listen to every single one until you can find the right sound. Even if the sounds do have some kind of word labels, it could be hard to pinpoint exactly which words to search for. A lot of sounds don’t exactly lend themselves to text descriptors, so finding the right sound can be difficult with a text search.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<div style="text-align:center">
  <iframe src="https://player.vimeo.com/video/491725663" width="640" height="360" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>
</div>
<p><em>The following is a transcript of the above video</em></p>
<p>In this paper, the authors present a novel neural network architecture to enable audio search via sounds humans are able to make, for example humming and whistling. This is an important capability when searching through audio for a specific sound.</p>
<h2 id="motivation">Motivation</h2>
<p>Imagine you have hundreds of unlabeled sound effects on your computer, and you are looking for a specific one. It could be very tedious to listen to every single one until you can find the right sound. Even if the sounds do have some kind of word labels, it could be hard to pinpoint exactly which words to search for. A lot of sounds don’t exactly lend themselves to text descriptors, so finding the right sound can be difficult with a text search.</p>
<p>This paper contains three main contributions:</p>
<p>First, it introduces a new neural network architecture for matching imitated sounds with a sound corpus, the semi-siamese convolutional neural network.</p>
<p>Second, the researchers built a second architecture which utilizes transfer learning from other audio tasks in an attempt for better performance.</p>
<p>Third, the researchers visualized and sonified input patterns which excited the neurons in different layers.</p>
<p>Both neural networks outperform the state of the art systems as we will see later on.</p>
<h2 id="data">Data</h2>
<p>To train the siamese model, the authors used a dataset called VocalSketch, which contains sounds from 4 broad categories: acoustic instruments, commercial synthesizers, everyday, and single synthesizer notes. The dataset also contains a number of human vocal imitations of each of the sounds.
For each of the 4 categories in VocalSketch, the researchers selected half of the sounds in each category along with corresponding vocal imitations as the training and validation set, and the other half for their test set.</p>
<p>Since each of the categories other than Everyday contained 40 sounds, 20 sounds would be for training and 20 for test and validation. The everyday category contained 120 sounds, so that category had 60 in training and 60 in test.
Each sound in the dataset has 10 corresponding human vocal imitations. In the training set, 7 of them for each sound were selected for training, and the remaining 3 were used for validation. Overall, the researchers used this dataset to create 840 positive and 840 negative pairs for training, and 360 positive and negative pairs for validation.</p>
<p>I thought it was interesting that the researchers opted not to use balanced categories of 20 sounds for each category. There isn’t a comment in the paper about the reasoning behind this but it may be due to the difficulty of categorizing this class of sounds.</p>
<p>The transfer learning model required pretraining the two towers on two additional datasets before training the full network on VocalSketch.</p>
<p>For the vocal imitation tower, they used a dataset called VoxForge. From this dataset the researchers selected 8 thousand samples for each of 7 different languages. They used a 70 - 30 split for training and testing, and achieved a 69.8% accuracy, which seems pretty good for a 7 class classification task.</p>
<p>For the second transfer learning tower, the environmental sound classification tower, they used a dataset called UrbanSound8k. This dataset contains 8732 sound samples in 10 different classes, things like car horns, jackhammers, and street music. The researchers used 10 fold cross validation when pretraining on this dataset and achieved a 70.2% accuracy over the dataset.</p>
<p>Note that for pre-training the two towers, the researchers used a slightly modified neural network architecture, appending two fully connected layers to categorize the results into the necessary number of classes.</p>
<p>So how are sounds fed into the neural networks? You’re probably familiar with the way that convolutional neural networks work with images. Typically the input for each image is of a shape width by height by color depth. Creating an input when working with sound is similar. The width of the audio file in this case is time, and the height is the frequencies during that time step. This is similar to the output of a spectrogram.</p>
<p>In order to be fed into the network, audio must first undergo a preprocessing step. The specific preprocessing involved varied between the networks, but generally involves downsampling the audio and splitting it by frequency band. This resulted in an input which resembles a spectrogram image.</p>
<h2 id="methodology">Methodology</h2>
<p>The heart of this problem is an architecture the researchers dubbed siamese style or semi siamese neural networks. A true siamese neural network consists of two identical neural network towers with the same weights, and is used similar to the way that hashing is used to match similar inputs. A siamese style network is similar to a siamese neural network, but may have different weights in one of its towers.</p>
<p>The first network the researchers called IMINET, which included a true siamese neural network as one of its configurations. This network consisted of two convolutional network towers, a concatenation step, and a fully-connected network with three more layers to compute a similarity score between 0 and 1. The convolutional neural networks in IMINET had the same structure, even though in some configurations their weights were not the same. They consisted of four convolutional layers with pool layers following convolutional layers 1 and 2.</p>
<p>The researchers detailed the parameters they used for each of the convolutional layers, specifically the number of filters and the receptive field size. Each of the filters in a layer learns to detect various characteristics of the input, and the receptive field is the part of the input that the filter is able to see. The max pooling layers output the maximum value among all of their inputs, reducing the number of inputs for the next layer in the network.</p>
<p>The researchers experimented with three different configurations for the convolutional network towers: a tied configuration, where both towers would share the exact same weights and biases; an untied configuration, where the two towers were required to share no weights and biases at all; and a partially tied configuration, where weights and biases were shared for convolutional layers 3 and 4 but not 1 and 2. Because the untied and partially tied configurations are not truly siamese neural networks, the researchers called these configurations semi siamese.</p>
<p>After both inputs pass through the convolutional towers, they are concatenated together into one input vector and fed into a fully connected network with three layers. This network’s job is to compute a similarity score between 0 and 1 for the two inputs. Both of the first two layers used ReLU activation. The final layer was a single neuron which used sigmoid activation to squash its input into an output between 0 and 1.</p>
<p>This architecture achieved state of the art results in sound retrieval which I will discuss in a moment along with the rest of the findings of this paper.</p>
<p>The second neural network developed by researchers was called TL-IMINET. This network was very similar to the first network, but this time the researchers tried using transfer learning to achieve better performance. The researchers hypothesized that the vocal imitation task shares many characteristics with language identification, and that sound recognition shares many similarities with environmental sound classification. This would allow networks which were pre-trained on these tasks to require only fine tuning to be adapted to this task.</p>
<p>The network architectures for the language identification and environmental classification tasks were slightly different from those used in IMINET and are shown here. Note that the towers are also different in architecture from each other.</p>
<p>The researchers also experimented with fusion strategies between different models. For IMINET, the similarity likelihoods for all three configurations were multiplied together to achieve a combined score. They also experimented with combining IMINET with the previous state of the art model. Since that model computes a cosine distance between the input sound and a candidate sound, this output was converted into a likelihood using softmax, and that softmax was multiplied by the output of IMINET. The transfer learning model TL-IMINET was also combined with the state of the art model in a similar way by computing the softmax and multiplying by the output of TL-IMINET. These fusion strategies ended up improving the performance of each of the models quite a bit.</p>
<h2 id="experiments-and-findings">Experiments and Findings</h2>
<p>To measure the performance of these networks, recall that the output of the networks was a number between 0 and 1 indicating how similar the network believed the two inputs were to each other. For example, two very similar inputs might have a similarity rating of 0.9, while two dissimilar inputs might have a similarity rating of 0.1. After gathering the similarity of the human vocalization sound to each of its potential matches, the matches were ranked according to their similarity score.</p>
<p>The authors then used a metric called mean reciprocal rank, which is a number between 0 and 1 indicating how well the algorithm ranked the sounds. For example, a mean reciprocal rank of 0.5 suggests that the target sound is ranked second among all possible matches on average.</p>
<p>Here are the performances of the various network configurations when measured by mean reciprocal rank. The researchers highlighted several insights which could be drawn from their results.</p>
<p>First, it seems that tied configurations performed the best among all configurations of IMINET. This runs contrary to the researchers’ expectations that untied configurations would outperform tied configurations.</p>
<p>Second, the tied configuration outperformed the previous state of the art benchmark in two categories of sounds: commercial synthesizers and everyday. It performed worse than the state of the art for acoustic instruments, and was about the same performance for the single synthesizer category.</p>
<p>Third, IMINET achieved even better performance in most categories by using an ensemble of different configurations.</p>
<p>Fourth, even without pretraining, the TL-IMINET model performed better than the untied configuration of IMINET for all categories except commercial synthesizers. This is interesting because the only difference between these two models is the network structure of the convolutional towers.</p>
<p>And finally, the pre-trained TL-IMINET model outperformed the previous state of the art model by quite a bit in all categories, but the best performing configuration overall was TL-IMINET fused with the previous state of the art model.</p>
<p>One of the most interesting experiments was the visualization of the input patterns which activate neurons in each of the layers. This was done by performing gradient ascent of the neuron activation with respect to the input from a random initialization state. Visualizing the convolutional layers showed that the first layer tends to learn local features like edges, intermediate layer neurons learn more complex features like texture and direction, and the deepest layer recognizes concentrations of frequency ranges. The visualizations also helped to confirm that pretraining indeed helped the networks to learn more detail. The patterns from the pretrained vocal imitation tower were sharper than those in the naive IMINET towers.</p>
<h2 id="takeaways">Takeaways</h2>
<p>There are a few key takeaways from this research. The first is that the transfer learners had much better performance than the naive non-transfer learner, as evidenced by the fact that TL-IMINET performed better than IMINET for most categories even though neither model was pretrained. The research also showed that ensemble methods can outperform any single model on its own. IMINET performed better when used in combination with its different configurations, and combining it with the state of the art model performed better than either model on its own. Finally, visualizing the inputs can help to confirm that the network is learning the correct things, and helps to provide insights as to what types of sound properties are important.</p>
]]></content>
        </item>
        
        <item>
            <title>A Practical Guide to Gaussian Mixture Models</title>
            <link>https://blog.lukesalamone.com/posts/gmm-practical-guide/</link>
            <pubDate>Sat, 24 Oct 2020 18:10:29 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/gmm-practical-guide/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    // Fix &lt;code&gt; tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;link rel=&#34;stylesheet&#34; href=&#34;../css/gmm-practical-guide-demo.css&#34; /&gt;
&lt;p&gt;Are you studying machine learning and want to know more about Gaussian Mixture Models? You&amp;rsquo;ve come to the right place. I have found other online resources to be difficult to approach and/or lacking crucial details. Here I will try to explain GMMs in plain language.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    // Fix <code> tags after MathJax finishes running. This is a
    // hack to overcome a shortcoming of Markdown. Discussion at
    // https://github.com/mojombo/jekyll/issues/199
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<link rel="stylesheet" href="/css/gmm-practical-guide-demo.css" />
<p>Are you studying machine learning and want to know more about Gaussian Mixture Models? You&rsquo;ve come to the right place. I have found other online resources to be difficult to approach and/or lacking crucial details. Here I will try to explain GMMs in plain language.</p>
<div class="gmm-demo" id="gmm-demo">
  <svg class="gmm-demo-chart" viewBox="0 0 600 200" preserveAspectRatio="none">
    <line class="gmm-demo-axis" x1="0" y1="176" x2="600" y2="176"></line>
    <path class="gmm-area gmm-area--green"></path>
    <path class="gmm-area gmm-area--blue"></path>
    <path class="gmm-area gmm-area--purple"></path>
    <path class="gmm-curve gmm-curve--green"></path>
    <path class="gmm-curve gmm-curve--blue"></path>
    <path class="gmm-curve gmm-curve--purple"></path>
    <line class="gmm-demo-guide" x1="0" y1="0" x2="0" y2="170"></line>
    <g id="gmm-demo-chevron" class="gmm-demo-chevron">
      <path d="M -8 0 L 0 -10 L 8 0 Z"></path>
    </g>
  </svg>
  <div class="gmm-demo-side">
    <label class="gmm-demo-slider">
      <span>Position</span>
      <input id="gmm-demo-slider" type="range" min="0" max="10" step="0.01" value="5" />
    </label>
    <div class="gmm-demo-probs">
      <div class="gmm-demo-prob">
        <div class="gmm-demo-bar">
          <div class="gmm-demo-bar-fill gmm-demo-bar-fill--green" data-prob="0"></div>
          <span class="gmm-demo-bar-text" data-prob-label="0">0%</span>
        </div>
      </div>
      <div class="gmm-demo-prob">
        <div class="gmm-demo-bar">
          <div class="gmm-demo-bar-fill gmm-demo-bar-fill--blue" data-prob="1"></div>
          <span class="gmm-demo-bar-text" data-prob-label="1">0%</span>
        </div>
      </div>
      <div class="gmm-demo-prob">
        <div class="gmm-demo-bar">
          <div class="gmm-demo-bar-fill gmm-demo-bar-fill--purple" data-prob="2"></div>
          <span class="gmm-demo-bar-text" data-prob-label="2">0%</span>
        </div>
      </div>
    </div>
  </div>
</div>
<script src="/js/gmm-practical-guide-demo.js"></script>
<h1 id="overview">Overview</h1>
<p>Gaussian Mixture Models are used to group data points into different clusters. In this way, it is similar to <a href="/posts/vectorized-kmeans/">K-means clustering</a>, where each cluster is specified by a point at its center. In a way, GMMs are a generalization of K-means, where we relax the assumption that the clusters are circular (or spherical, or n-dimensional sphere, depending on how many dimensions your features have). Gaussians are specified by three parameters. Just like how the K-means algorithm seeks a point which best &ldquo;fits&rdquo; your data points, the GMM algorithm will seek a gaussian which best fits them.</p>
<p>Any GMM model you will be implementing will probably have a <code>fit</code> and a <code>predict</code> method. The goal of the <code>fit</code> method is to find a number of gaussians to cluster your data points. The <code>predict</code> method takes a new point (or a list of new points) and predicts which gaussian is closest to it. So you may have a skeleton class like so:</p>
<pre><code class="language-python">class GMM():
    def __init__(self, n_clusters, covariance_type):
      pass

    def fit(self, features):
      pass

    def predict(self, features):
      pass
</code></pre>
<h1 id="the-fit-method">The fit() method</h1>
<p>This method has two major sub-parts: the <em>expectation step</em>, and the <em>Maximization step</em>. These two parts will be run inside of an update loop which typically terminates after a set number of iterations or the algorithm has stopped improving.</p>
<p>I personally find that examples are the best way to understand many concepts, so in this article I will use a dataset with 4 features, each of the features having 3 dimensions:</p>
<table>
	<thead>
			<tr>
					<th>feature</th>
					<th>d<sub>0</sub></th>
					<th>d<sub>1</sub></th>
					<th>d<sub>2</sub></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>x<sub>0</sub></td>
					<td>x<sub>00</sub></td>
					<td>x<sub>01</sub></td>
					<td>x<sub>02</sub></td>
			</tr>
			<tr>
					<td>x<sub>1</sub></td>
					<td>x<sub>10</sub></td>
					<td>x<sub>11</sub></td>
					<td>x<sub>12</sub></td>
			</tr>
			<tr>
					<td>x<sub>2</sub></td>
					<td>x<sub>20</sub></td>
					<td>x<sub>21</sub></td>
					<td>x<sub>22</sub></td>
			</tr>
			<tr>
					<td>x<sub>3</sub></td>
					<td>x<sub>30</sub></td>
					<td>x<sub>31</sub></td>
					<td>x<sub>32</sub></td>
			</tr>
	</tbody>
</table>
<h2 id="expectation-step">Expectation Step</h2>
<p>The purpose of this step is to calculate for each of our features, the probability of that feature belonging to cluster k. In this example we have 4 clusters and 4 data points. So we need to fill in the following table:</p>
<table>
	<thead>
			<tr>
					<th>feature</th>
					<th>k<sub>0</sub></th>
					<th>k<sub>1</sub></th>
					<th>k<sub>2</sub></th>
					<th>k<sub>3</sub></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>x<sub>0</sub></td>
					<td>γ<sub>00</sub></td>
					<td>γ<sub>01</sub></td>
					<td>γ<sub>02</sub></td>
					<td>γ<sub>03</sub></td>
			</tr>
			<tr>
					<td>x<sub>1</sub></td>
					<td>γ<sub>10</sub></td>
					<td>γ<sub>11</sub></td>
					<td>γ<sub>12</sub></td>
					<td>γ<sub>13</sub></td>
			</tr>
			<tr>
					<td>x<sub>2</sub></td>
					<td>γ<sub>20</sub></td>
					<td>γ<sub>21</sub></td>
					<td>γ<sub>22</sub></td>
					<td>γ<sub>23</sub></td>
			</tr>
			<tr>
					<td>x<sub>3</sub></td>
					<td>γ<sub>30</sub></td>
					<td>γ<sub>31</sub></td>
					<td>γ<sub>32</sub></td>
					<td>γ<sub>33</sub></td>
			</tr>
	</tbody>
</table>
<h2 id="maximization-step">Maximization Step</h2>
<p>In this step we need to update three variables for each of the clusters: the weight, the mean, and the diagonal covariance. In this article I will use w<sub>j</sub> to represent the weight of the j<sup>th</sup> gaussian, μ<sub>j</sub> to represent the j<sup>th</sup> mean, and σ<sub>j</sub> to represent the j<sup>th</sup> variance.</p>
<h3 id="calculating-wj">Calculating w<sub>j</sub></h3>
<p>The first variable will be r<sub>j</sub>, which is the sum of gammas corresponding to a given cluster k<sub>j</sub>. In other words,</p>
<p style="text-align: center; font-size: 1.5em">
$ r_{j} = \sum_{i=0}^3 γ_{ij} $
</p>
<p>Basically, r<sub>j</sub> is the sum of the gammas in the j<sup>th</sup> column. We will use this r<sub>j</sub> to calculate a w<sub>j</sub> for each cluster, the &ldquo;mixing weight&rdquo; of the gaussian. Divide by the number of features (in this case 4):</p>
<p style="text-align: center; font-size: 1.5em">
$ w_{j} = {r_{j} \over 4} $
</p>
<h3 id="calculating-μj">Calculating μ<sub>j</sub></h3>
<p>Next we calculate the means μ<sub>j</sub> of the gaussians i.e. the point in n-dimensional space over which the (n+1)-dimensional bell curve is centered. Because in our example each feature x<sub>i</sub> contains 3 dimensions (x<sub>0</sub> = [d<sub>00</sub>, d<sub>01</sub>, d<sub>02</sub>]), the means will also need to have 3 dimensions each.</p>
<p>μ<sub>j</sub> is calculated as follows:</p>
<p style="font-size: 1.5em">
$$ μ_{j} = {1 \over r_{j} } \sum_{i=0}^3 γ_{ji} x_{i} $$
</p>
<p>Let&rsquo;s work through the first μ. For simplicity we will multiply by r<sub>j</sub> for now. For the first mean μ<sub>0</sub>:</p>
<p style="font-size: 1.5em">
$$ r_j μ_0 = γ_{00} x_{0} + γ_{01} x_{1} + γ_{02} x_{2} + γ_{03} x_{3} $$
</p>
<p style="font-size: 1.5em">
$$ = γ_{00} \begin{bmatrix}
d_{00} \\
d_{01} \\
d_{02}
\end{bmatrix} +
γ_{01}
\begin{bmatrix}
d_{10} \\
d_{11} \\
d_{12}
\end{bmatrix} +
γ_{02}
\begin{bmatrix}
d_{20} \\
d_{21} \\
d_{22}
\end{bmatrix} +
γ_{03}
\begin{bmatrix}
d_{30} \\
d_{31} \\
d_{32}
\end{bmatrix}$$
</p>
<p>So we are essentially multiplying the gammas associated with a gaussian by all of the features we have. This matrix multiplication can be rolled up into</p>
<p style="font-size: 1.5em">
$$ = \begin{bmatrix}
d_{00} & d_{10} & d_{20} & d_{30} \\
d_{01} & d_{11} & d_{21} & d_{31} \\
d_{02} & d_{12} & d_{22} & d_{32}
\end{bmatrix}
\begin{bmatrix}
γ_{00} \\
γ_{01} \\
γ_{02} \\
γ_{03} \\
\end{bmatrix}
= \begin{bmatrix}
μ_{00} \\
μ_{01} \\
μ_{02}
\end{bmatrix}
$$
</p>
<p>Don&rsquo;t forget to divide by r<sub>j</sub> again! It makes sense that each μ should have 3 dimensions, since our features also each have 3 dimensions.</p>
<h3 id="calculating-σj">Calculating σ<sub>j</sub></h3>
<p>Technically this is σ<sup>2</sup> if you&rsquo;re here from statistics, but since we never take the square root it doesn&rsquo;t matter for our purposes.</p>
<h3 id="all-together-now-e-step-with-code">All together now. E step with code.</h3>
<p>The full equation for the expectation step is</p>
<p style="font-size: 1.5em">
$$
\gamma_{ij}
=
\frac{
w_j \, \mathcal{N}\!\left(x_i \mid \mu_j, \Sigma_j\right)
}{
\sum_{k=0}^{K-1}
w_k \, \mathcal{N}\!\left(x_i \mid \mu_k, \Sigma_k\right)
}
$$
</p>
<p>Alright, enough math. As fun as it is for you to read, it&rsquo;s even more fun to write in markdown.</p>
<p>All of the math above for calculating w, μ and σ can be written succinctly in python. Here I am using <a href="https://numpy.org/">numpy</a> to take advantage of some pretty slick matrix operations they have available.</p>
<pre><code class="language-python">&quot;&quot;&quot;
Fit GMM to the given data using self.n_clusters number of gaussians.
Features may be multi-dimensional.

Args:
  features: numpy array containing inputs of size
    (n_samples, n_dimensions)
  expectations: numpy array containing gammas from E step. Size is
    (n_features, n_clusters)
Returns:
  means: updated means
  covariances: updated covariances
  mixing_weights: updated mixing weights
&quot;&quot;&quot;

def maximization_step(self, features, expectations):
  # initialize lists to hold our parameters
  mixing_weights, means, covariances = [], [], []

  for cluster in range(self.n_clusters):
    # gammas will hold a column from the expectations matrix
    gammas = expectations[:,cluster]

    # r is just the sum of a column
    r = np.sum(gammas)

    # calculate mixing weight
    w = r / len(features)
    mixing_weights.append(w)

    # calculate means
    # the @ symbol is a slick shortcut for matrix multiplication
    mu = (gammas @ features) / r
    means.append(mu)

    # calculate covariances
    diff_sq = (features-mu) ** 2
    cov = (gammas @ diff_sq) / r
    covariances.append(cov)

  return means, covariances, mixing_weights
</code></pre>
<h1 id="the-predict-step">The predict() step</h1>
<p>Once the model is trained, we have learned:</p>
<ul>
<li>Mixing weights w<sub>j</sub></li>
<li>Means μ<sub>j</sub></li>
<li>Covariances Σ<sub>j</sub></li>
</ul>
<p>To classify a new data point <code>x</code> we compute its responsibilities using the same formula from the E-step:</p>
<p style="font-size: 1.5em">
$$
\gamma_j(x)
=
\frac{
w_j \, \mathcal{N}\!\left(x \mid \mu_j, \Sigma_j\right)
}{
\sum_{k=0}^{K-1}
w_k \, \mathcal{N}\!\left(x \mid \mu_k, \Sigma_k\right)
}
$$
</p>
<p>This yields a probability distribution over all clusters. If we only want the most likely cluster,</p>
<p style="font-size: 1.5em">
$$
\text{cluster}(x) = \arg\max_{j} \, \gamma_j(x)
$$
</p>
<pre><code class="language-python">def predict_probs(self, features):
    gamma, _ = self._e_step(features)
    return gamma

def predict(self, features):
    return np.argmax(self.predict_probs(features), axis=1)
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>Managing Python Environments</title>
            <link>https://blog.lukesalamone.com/posts/managing-python-environments/</link>
            <pubDate>Sat, 24 Oct 2020 17:45:41 -0500</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/managing-python-environments/</guid>
            <description>&lt;p&gt;Need to switch between python versions often? Use &lt;a href=&#34;https://github.com/pyenv/pyenv&#34;&gt;&lt;code&gt;pyenv&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;installing-pyenv&#34;&gt;Installing pyenv&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# install pyenv
curl https://pyenv.run | bash

# check pyenv install location
which pyenv
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;install-another-python-version&#34;&gt;Install another python version&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# see a list of available python versions
pyenv install --list

# check installed python versions
pyenv versions

# installs python 3.7.5
pyenv install 3.7.5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;switch-python-versions&#34;&gt;Switch python versions&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# use python 3.7.5 everywhere on your machine
pyenv global 3.7.5

# use python 3.7.5 in current directory
pyenv local 3.7.5

# use python 3.7.5 in current shell session
pyenv shell 3.7.5
&lt;/code&gt;&lt;/pre&gt;</description>
            <content type="html"><![CDATA[<p>Need to switch between python versions often? Use <a href="https://github.com/pyenv/pyenv"><code>pyenv</code></a>.</p>
<h3 id="installing-pyenv">Installing pyenv</h3>
<pre><code class="language-bash"># install pyenv
curl https://pyenv.run | bash

# check pyenv install location
which pyenv
</code></pre>
<h3 id="install-another-python-version">Install another python version</h3>
<pre><code class="language-bash"># see a list of available python versions
pyenv install --list

# check installed python versions
pyenv versions

# installs python 3.7.5
pyenv install 3.7.5
</code></pre>
<h3 id="switch-python-versions">Switch python versions</h3>
<pre><code class="language-bash"># use python 3.7.5 everywhere on your machine
pyenv global 3.7.5

# use python 3.7.5 in current directory
pyenv local 3.7.5

# use python 3.7.5 in current shell session
pyenv shell 3.7.5
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>How does K-means clustering work?</title>
            <link>https://blog.lukesalamone.com/posts/kmeans-clustering/</link>
            <pubDate>Wed, 07 Oct 2020 17:39:22 -0700</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/kmeans-clustering/</guid>
            <description>&lt;p&gt;K-means clustering (not to be confused with K-nearest neighbors) is an unsupervised learning algorithm used for grouping similar points together into clusters.&lt;/p&gt;
&lt;script src=&#34;https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js&#34;&gt;&lt;/script&gt;
&lt;script src=&#34;../js/kmeans_demo.js&#34;&gt;&lt;/script&gt;
&lt;div id=&#39;demo&#39;&gt;
&lt;button&gt;start&lt;/button&gt;
&lt;canvas id=&#34;myChart&#34; style=&#34;background-color: #0000&#34;&gt;&lt;/canvas&gt;
&lt;/div&gt;
&lt;h2 id=&#34;algorithm&#34;&gt;Algorithm&lt;/h2&gt;
&lt;p&gt;The basic K-means algorithm is fairly simple and has two steps, repeated until convergence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;assign points to cluster corresponding to closest centroid&lt;/li&gt;
&lt;li&gt;update centroid locations to the mean of all points assigned to the associated cluster&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The algorithm converges when the centroids stop moving, i.e. no points can be switched between clusters to a closer centroid.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>K-means clustering (not to be confused with K-nearest neighbors) is an unsupervised learning algorithm used for grouping similar points together into clusters.</p>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<script src="/js/kmeans_demo.js"></script>
<div id='demo'>
<button>start</button>
<canvas id="myChart" style="background-color: #0000"></canvas>
</div>
<h2 id="algorithm">Algorithm</h2>
<p>The basic K-means algorithm is fairly simple and has two steps, repeated until convergence:</p>
<ol>
<li>assign points to cluster corresponding to closest centroid</li>
<li>update centroid locations to the mean of all points assigned to the associated cluster</li>
</ol>
<p>The algorithm converges when the centroids stop moving, i.e. no points can be switched between clusters to a closer centroid.</p>
<pre><code class="language-python">def kmeans(points, k):

  random.shuffle(points)
  centroids = points[:k]
  done = False

  while not done:
    clusters = clear_clusters()

    # assign each point to closest cluster
    for point in points:
      cluster_idx = closest_cluster(centroids, point)
      clusters[cluster_idx].append(point)

    # recalculate centroids
    new_centroids = update_centroids(clusters)
    done = new_centroids == centroids
  return clusters
</code></pre>
]]></content>
        </item>
        
        <item>
            <title>What is the Hardest Hangman Word?</title>
            <link>https://blog.lukesalamone.com/posts/hardest-hangman-word/</link>
            <pubDate>Tue, 21 Jul 2020 17:34:05 +0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/hardest-hangman-word/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://i.imgur.com/p33HisS.png&#34; alt=&#34;Example hangman game&#34;&gt;&lt;/p&gt;
&lt;p&gt;It seems like a simple enough question. Which word should you choose so that it takes your opponent the most guesses to discover it? Should you choose a long word to use up your opponent&amp;rsquo;s guesses? Or perhaps a short word with obscure letters? In this document I look into this question. But first, a bit of background.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re not familiar with the rules of hangman, it is a guessing game played between two people. Player A chooses a secret word, and tells player B the length of the secret word. Player B guesses letters which she thinks might be in the word. If she chooses a correct letter, player A reveals the locations of &lt;em&gt;each instance&lt;/em&gt; of the guessed letter. However, if player B guesses an incorrect letter, this counts as a &amp;ldquo;strike&amp;rdquo; against her. After an agreed-upon number of strikes, player B loses.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><img src="https://i.imgur.com/p33HisS.png" alt="Example hangman game"></p>
<p>It seems like a simple enough question. Which word should you choose so that it takes your opponent the most guesses to discover it? Should you choose a long word to use up your opponent&rsquo;s guesses? Or perhaps a short word with obscure letters? In this document I look into this question. But first, a bit of background.</p>
<p>If you&rsquo;re not familiar with the rules of hangman, it is a guessing game played between two people. Player A chooses a secret word, and tells player B the length of the secret word. Player B guesses letters which she thinks might be in the word. If she chooses a correct letter, player A reveals the locations of <em>each instance</em> of the guessed letter. However, if player B guesses an incorrect letter, this counts as a &ldquo;strike&rdquo; against her. After an agreed-upon number of strikes, player B loses.</p>
<h1 id="an-algorithmic-approach">An algorithmic approach</h1>
<p>A few years ago, I created a &ldquo;hangman solver&rdquo; for the popular paper and pencil game. This game assessed each game analytically, to determine a list of possible words given the clues available. The algorithm works as follows: at the beginning of the game, we know the length of the secret word, which narrows our dictionary considerably. Then, for each letter in the alphabet, count the number of words available which contain that letter.</p>
<p>Suppose our dictionary consisted of a random list of 50 four-letter words as follows:</p>
<pre><code class="language-javascript">[&quot;pull&quot;, &quot;dipt&quot;, &quot;dorp&quot;, &quot;poky&quot;, &quot;jism&quot;, &quot;cues&quot;, &quot;hood&quot;, &quot;drag&quot;,
&quot;inky&quot;, &quot;mhos&quot;, &quot;kerf&quot;, &quot;jess&quot;, &quot;mete&quot;, &quot;lues&quot;, &quot;wipe&quot;, &quot;kane&quot;,
&quot;tiro&quot;, &quot;keys&quot;, &quot;jape&quot;, &quot;lime&quot;, &quot;sees&quot;, &quot;sass&quot;, &quot;demo&quot;, &quot;ilia&quot;,
&quot;mink&quot;, &quot;dips&quot;, &quot;hove&quot;, &quot;jees&quot;, &quot;that&quot;, &quot;pops&quot;, &quot;isle&quot;, &quot;teas&quot;,
&quot;dens&quot;, &quot;dogy&quot;, &quot;pink&quot;, &quot;sizy&quot;, &quot;cole&quot;, &quot;pact&quot;, &quot;thaw&quot;, &quot;lead&quot;,
&quot;mile&quot;, &quot;dodo&quot;, &quot;litu&quot;, &quot;scup&quot;, &quot;colt&quot;, &quot;soma&quot;, &quot;seat&quot;, &quot;dewy&quot;,
&quot;pits&quot;, &quot;mojo&quot;]
</code></pre>
<p>This would result in letter counts as follows:</p>
<table>
	<thead>
			<tr>
					<th>letter</th>
					<th>count</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>a</td>
					<td>16</td>
			</tr>
			<tr>
					<td>b</td>
					<td>7</td>
			</tr>
			<tr>
					<td>c</td>
					<td>15</td>
			</tr>
			<tr>
					<td>d</td>
					<td>5</td>
			</tr>
			<tr>
					<td>e</td>
					<td>26</td>
			</tr>
			<tr>
					<td>f</td>
					<td>1</td>
			</tr>
			<tr>
					<td>g</td>
					<td>5</td>
			</tr>
			<tr>
					<td>h</td>
					<td>3</td>
			</tr>
			<tr>
					<td>i</td>
					<td>10</td>
			</tr>
			<tr>
					<td>j</td>
					<td>1</td>
			</tr>
			<tr>
					<td>k</td>
					<td>6</td>
			</tr>
			<tr>
					<td>l</td>
					<td>14</td>
			</tr>
			<tr>
					<td>m</td>
					<td>6</td>
			</tr>
			<tr>
					<td>n</td>
					<td>6</td>
			</tr>
			<tr>
					<td>o</td>
					<td>14</td>
			</tr>
			<tr>
					<td>p</td>
					<td>7</td>
			</tr>
			<tr>
					<td>r</td>
					<td>8</td>
			</tr>
			<tr>
					<td>s</td>
					<td>15</td>
			</tr>
			<tr>
					<td>t</td>
					<td>13</td>
			</tr>
			<tr>
					<td>u</td>
					<td>4</td>
			</tr>
			<tr>
					<td>w</td>
					<td>1</td>
			</tr>
			<tr>
					<td>x</td>
					<td>1</td>
			</tr>
			<tr>
					<td>y</td>
					<td>4</td>
			</tr>
	</tbody>
</table>
<p>In this case, the letter E is found in 26 words, the most of any letter. Therefore our algorithm should pick E since it is the safest guess. Once we guess a letter correctly, this gives important positional information which can filter the word list even further.</p>
<p>This process is repeated, each time picking the most likely letter, given the constraints we know. If we know some of the letters in the secret word, we can eliminate any words that don&rsquo;t have those letters in those positions. If we have guessed a letter incorrectly, we know that letter isn&rsquo;t in the secret word and can eliminate all words which have that letter. You can see a python implementation of this algorithm <a href="https://gist.github.com/lukesalamone/a815cda5e427b28db78e0caafdbbc0f3#file-hangman_solver-py">here</a>.</p>
<p>For this experiment I used the <a href="http://www.blogmybrain.com/words-with-friends-cheat/words.txt">Zynga dictionary</a>, the same dictionary used in the game Words With Friends. The dictionary contains 173,000 words. It does not include any proper nouns or profanities.</p>
<h1 id="live-experiment">Live experiment</h1>
<p>Below you can experiment with this algorithm among 4 letter words. Enter a secret word, and the steps used to uncover the secret word will be displayed below. (Need inspiration? Try comparing <em>jazz</em> to <em>rock</em> or <em>blue</em> to <em>grey</em>.)</p>
<div id="demo1" style="margin:20px;background:#ddd;padding:20px;font-family:monospace">
    <script src="/js/hangman-demo1.js"></script>
    <div style="width:100%; text-align:center">
        <input placeholder="enter 4 letter word"></input>
        <button class="play">play word</button>
        <button class="reset">reset</button>
    </div>
    <div class="output" style="margin: 20px 0"></div>
</div>
<p>Notice how some words have much higher difficulty than others? This is due to the fact that some words have many &ldquo;siblings&rdquo; which differ by only one letter. For example the pattern &ldquo;.ays&rdquo; could match the letters c, d, f, h, j, k, l, n, or r. Knowing the last 3 letters gives us no indication of which of these nine letters it will be.</p>
<p>One apparent shortcoming of the above calculation is that it assumes that letters with equal probability will be picked in alphabetical order, and therefore letters last in the alphabet will be picked last. Although this has the benefit of creating a deterministic algorithm which will always return the same result for the same word, in real life we don&rsquo;t know in which order people will pick letters. In actuality the words <em>days</em>, <em>jays</em> and <em>rays</em> have equal difficulty each is equally likely to be the secret word.</p>
<h1 id="preliminary-results">Preliminary results</h1>
<p>With this preliminary caveat in mind, we can still calculate the difficulty of every word in the dictionary. If we assume that in a situation where multiple letters are equally probable, our opponent will break the tie using alphabetical ordering, which hangman words are the hardest to guess? Here are the top 17:</p>
<table>
	<thead>
			<tr>
					<th>word</th>
					<th>difficulty</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>zill</td>
					<td>19</td>
			</tr>
			<tr>
					<td>zills</td>
					<td>18</td>
			</tr>
			<tr>
					<td>yill</td>
					<td>18</td>
			</tr>
			<tr>
					<td>zin</td>
					<td>17</td>
			</tr>
			<tr>
					<td>zax</td>
					<td>17</td>
			</tr>
			<tr>
					<td>yills</td>
					<td>17</td>
			</tr>
			<tr>
					<td>will</td>
					<td>17</td>
			</tr>
			<tr>
					<td>vox</td>
					<td>17</td>
			</tr>
			<tr>
					<td>mem</td>
					<td>17</td>
			</tr>
			<tr>
					<td>zins</td>
					<td>16</td>
			</tr>
			<tr>
					<td>yuck</td>
					<td>16</td>
			</tr>
			<tr>
					<td>yin</td>
					<td>16</td>
			</tr>
			<tr>
					<td>wills</td>
					<td>16</td>
			</tr>
			<tr>
					<td>vill</td>
					<td>16</td>
			</tr>
			<tr>
					<td>oak</td>
					<td>16</td>
			</tr>
			<tr>
					<td>jazz</td>
					<td>16</td>
			</tr>
			<tr>
					<td>foy</td>
					<td>16</td>
			</tr>
			<tr>
					<td>(27 more)</td>
					<td>15</td>
			</tr>
	</tbody>
</table>
<h1 id="previous-research">Previous research</h1>
<p>I should note that <a href="https://web.archive.org/web/20100815092214/http://blog.wolfram.com/2010/08/13/25-best-hangman-words/">previous research by Jon McLoone in 2010</a> has explored the same topic, although he used slightly different methodology and a smaller 90,000 word dictionary. His algorithm was not deterministic, and so does not always pick the most frequent letter available. Instead, his algorithm picks a letter with a probability proportional to the frequency with which it occurs in candidate words. For example, if we refer to the letter frequencies of the 50 word 4-letter dictionary above, <em>j</em> appears in just 1 word, while <em>e</em> appears in 26 of them. In this case, since the sum of the numbers in the frequency table is 188, McLoone&rsquo;s algorithm would pick <em>z</em> in 1 out of every 188 first guesses, and <em>e</em> in 26 of them.  Although it might seem that this strategy is not optimal, it does avoid the deterministic results shown above.</p>
<p>Additionally, McLoone chose to remain faithful to the logic of the hangman game, opting to end the games after a given number of mistakes, and recording the probability that a given word was not discovered after the game ended. So an 8-game means that the game was ended after 8 mistakes, and a 13-game after 13 mistakes. Using this methodology, he found the hardest hangman words were as follows:</p>
<p><img src="https://i.imgur.com/N5ginNw.gif" alt="Jon McLoone results"></p>
<h1 id="future-research">Future research</h1>
<p>Now, I think that we can improve upon our results a bit. Rather than calculating difficulty deterministically, we can instead randomize the ordering which letters will be picked in. This should dramatically reduce some of the outliers from above, bringing &ldquo;rays&rdquo; down from a difficulty of 14 to something more reasonable.</p>
<p>Such a stochastic calculation will require simulating millions of games. For my 173,000 word dictionary, simply simulating 10 games would involve 1.7 million games. Fortunately, this operation is highly parallelizable. It should be possible to split the dictionary into 100 or even 1000 pieces and derive the results for each piece simultaneously.</p>
<p>A simulation of this algorithm is shown below, with a graph of the average number of mistakes the new randomized algorithm incurs before discovering your secret word. The simulation is set up to play 500 rounds of games, and the final average is displayed at the top.</p>
<div id="demo2" style="margin:20px;background:#ddd;padding:20px;font-family:monospace">
    <script src="/js/chart.min.js"></script>
    <script src="/js/hangman-demo2.js"></script>
    <div style="width:100%; text-align:center">
        <input placeholder="enter 4 letter word"></input>
        <button class="play">play word</button>
        <button class="reset">reset</button>
    </div>
    <div class="messages"><span></span></div>
    <div class="canvas-holder" style="display:none;width:500px;height:500px">
        <canvas></canvas>
    </div>
</div>
]]></content>
        </item>
        
        <item>
            <title>Estimating Pi with a Monte Carlo Simulation</title>
            <link>https://blog.lukesalamone.com/posts/monte-carlo/</link>
            <pubDate>Thu, 09 Jul 2020 15:40:14 +0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/monte-carlo/</guid>
            <description>&lt;script type=&#34;text/javascript&#34; async src=&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&#34;&gt;&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [[&#39;$&#39;,&#39;$&#39;], [&#39;\\(&#39;,&#39;\\)&#39;]],
    displayMath: [[&#39;$$&#39;,&#39;$$&#39;], [&#39;\[&#39;,&#39;\]&#39;]],
    processEscapes: true,
    processEnvironments: true,
    skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;],
    TeX: {
      equationNumbers: {
        autoNumber: &#34;AMS&#34;
      },
      extensions: [&#34;AMSmath.js&#34;, &#34;AMSsymbols.js&#34;]
    }
  }
});
&lt;/script&gt;
&lt;script type=&#34;text/x-mathjax-config&#34;&gt;
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i &lt; all.length; i += 1) {
        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;
    }
});
&lt;/script&gt;
&lt;p&gt;A Monte Carlo simulation is a method of estimating events or quantities which are difficult or computationally infeasible to derive a closed-form solution to. The value of the mathematical constant Pi is a good example of this: although it is possible to calculate the exact value of Pi, a good estimate is easily demonstrated with just a few lines of code.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\[','\]']],
    processEscapes: true,
    processEnvironments: true,
    skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
    TeX: {
      equationNumbers: {
        autoNumber: "AMS"
      },
      extensions: ["AMSmath.js", "AMSsymbols.js"]
    }
  }
});
</script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i = 0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';
    }
});
</script>
<p>A Monte Carlo simulation is a method of estimating events or quantities which are difficult or computationally infeasible to derive a closed-form solution to. The value of the mathematical constant Pi is a good example of this: although it is possible to calculate the exact value of Pi, a good estimate is easily demonstrated with just a few lines of code.</p>
<p>Monte Carlo simulations work when the input can be drawn from a random probability distribution, and the outcome can be derived deterministically from the input. In the case of calculating Pi, this can be modeled geometrically. The <em>random distribution</em> is all points within the square, and the outcome is whether a selected point lies within the circle inside of the square. We know that for a square circumscribed about a circle,</p>
<p style="text-align: center; font-size: 1.5em">
$$
Area_{circle} = \pi r^2, Area_{square} = (2r)^2 = 4r^2
$$
<p>$$
\frac{Area_{circle}}{Area_{square}} = \frac{\pi r^2}{4r^2} = \frac{\pi}{4}
$$</p>
<p>$$
4(\frac{Area_{circle}}{Area_{square}}) = \pi
$$</p>
</p>
<p>If we notice that the probability that a randomly placed dot will fall within the circle is the same as the ratio of their areas (i.e. the circle takes up about 78% of the area of the square, so a random dot has about a 78% chance of landing inside the circle), then multiplying that probability by 4 gives Pi. By placing dots randomly, we play out that probability in real-time.</p>
<p>As to whether a given dot lies within the circle, we simply use the Pythagorean theorem to calculate its distance from the origin:</p>
<p style="text-align: center; font-size: 1.5em">
<p>$$ \sqrt{ x^2 + y^2 } &lt; 1  $$</p>
</p>
<p>In other words, all dots greater than 1 unit from the origin are outside the circle.</p>
<p>Below is a simulation of the derivation of the value of Pi. Click &ldquo;start simulation&rdquo; to see for yourself. <strong>Warning:</strong> this simulation may become slow once many dots are drawn on the screen.
<script src="/js/chart.min.js" type="text/javascript"></script>
<script src="/js/monte-carlo.js" type="text/javascript"></script></p>
<div id="sim1" style="font-family: monospace; background-color: #444; margin: 20px 0">
<div style="padding: 20px; text-align: center">
    <button class="start">start simulation</button>
    <button class="stop" disabled>stop simulation</button>
</div>
<div style="width:500px; height:500px">
    <canvas></canvas>
</div>
<div style="color: #fff; margin-left: 20px;">
    <p style="margin:0" id="num-points">Number of points: 0</p>
    <p style="margin:0" id="pct-inside">% inside circle: 0</p>
    <p style="margin:0" id="pi">Approximate value of pi: 0</p>
</div>
</div>
<p>Next, I was interested in the way that this simulation would play out once the number of points became large. Intuitively, this should approach Pi, but doing so requires that the random numbers generated by browsers be evenly distributed.</p>
<div id="sim2" style="font-family: monospace; background-color: #444; margin: 20px 0">
    <div style="padding: 20px; text-align: center">
        <button class="start">start simulation</button>
        <button class="stop" disabled>stop simulation</button>
    </div>
    <div style="width:600px; height:700px">
        <canvas></canvas>
    </div>
    <div style="color: #fff; margin-left: 20px;">
        <p style="margin:0" class="num-points">Number of points: 0</p>
        <p style="margin:0" class="pi">Approximate value of pi: 0</p>
        <p style="margin:0" class="pct-error">% error: 0</p>
    </div>
</div>
]]></content>
        </item>
        
        <item>
            <title>Creating an AI for Gomoku</title>
            <link>https://blog.lukesalamone.com/posts/gomoku2049/</link>
            <pubDate>Tue, 19 May 2020 14:28:57 +0800</pubDate>
            
            <guid>https://blog.lukesalamone.com/posts/gomoku2049/</guid>
            <description>&lt;p&gt;Gomoku is a strategy game similar to tic tac toe, but played on a larger board and with the goal of getting 5 in a row rather than 3. Since the game has perfect information and has simple rules, I thought it would be a fun exercise in creating a game AI.&lt;/p&gt;
&lt;p&gt;In February 2020 I decided to code up Gomoku2049. The game is a demonstration of MiniMax, which is &lt;a href=&#34;../posts/chess-engine-history/&#34;&gt;an algorithm for finding the move which minimizes the opponent’s best moves&lt;/a&gt;. This article is an overview of the game’s technical highlights.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Gomoku is a strategy game similar to tic tac toe, but played on a larger board and with the goal of getting 5 in a row rather than 3. Since the game has perfect information and has simple rules, I thought it would be a fun exercise in creating a game AI.</p>
<p>In February 2020 I decided to code up Gomoku2049. The game is a demonstration of MiniMax, which is <a href="/posts/chess-engine-history/">an algorithm for finding the move which minimizes the opponent’s best moves</a>. This article is an overview of the game’s technical highlights.</p>
<p><a href="http://gomoku2049.s3-website-us-east-1.amazonaws.com/">Click here to try out the game!</a></p>
<h2 id="minimax-with-alpha-beta-pruning">Minimax with alpha-beta pruning</h2>
<figure><img src="/img/game-tree.png"
			alt="In the tree above, the current game is shown on the left, green to move. If green fails to block orange’s 3 in a row now, orange cannot be stopped."><figcaption>
			<p>In the tree above, the current game is shown on the left, green to move. If green fails to block orange’s 3 in a row now, orange cannot be stopped.</p>
		</figcaption>
</figure>

<p>The Minimax algorithm represents every game as a tree of moves, with the current game position at the root of the tree. The algorithm is recursive with exponential time complexity and can have a very high branching factor: after the first move there are 225–1=224 possible moves. Because it is not feasible to evaluate all possible games to completion, Minimax calculation is usually limited to a fixed depth, after which the algorithm evaluates terminal leaf nodes using the gameover function and the static evaluator.</p>
<p>After each human move (known as “plies”), Minimax assigns a score to each of the possible reply moves. By convention, the AI will score favorable moves with a positive score, and unfavorable moves with a negative score. The move corresponding to the highest score is then selected. In other words, the AI is called the “maximizer”. Likewise, the human is known as the minimizer. To determine the score of each possible move, the minimax algorithm will recursively either maximize or minimize the possible moves available. After a given depth, the evaluation will stop, and return either an infinite value (+∞ for an AI win, -∞ for human win) or a finite evaluation of the state of the board. This static evaluation can be rather expensive, but luckily even a rough approximation is effective.</p>
<p>In practice, in addition to a depth limitation, this minimax algorithm also reduces the branching factor by limiting the squares which will be evaluated to those which are adjacent to squares which have been played. Given the fact that a disconnected “island” square cannot immediately lead to a win, this seems to be a reasonable simplification.
At the leaf nodes of the tree, either the game is over (the human has won or the computer has won) or the board needs to be evaluated with regards to who is winning.</p>
<h2 id="alpha-beta-pruning">Alpha-beta pruning</h2>
<p>Alpha-beta pruning is an improvement on the minimax algorithm, reducing the number of branches and leaf nodes which need to be evaluated. This is achieved by “pruning” unnecessary branches, ignoring them because the parent minimizer/maximizer would never choose it. For a maximizer (whose parent is a minimizer), this will occur if the parent minimizer has already seen a lower evaluation than a number the maximizing child sees.</p>
<h2 id="static-evaluator">Static evaluator</h2>
<p>This function is used to evaluate a board position with regards to which player is winning, and by how much. The MiniMax algorithm will then choose the highest value for itself, while minimizing the options for its opponent. For gomoku, it was important to derive an evaluation function which could be calculated quickly, and which builds towards the final desired result of 5 squares in a row. Note that such a function would necessarily be isomorphic in four directions: vertical, horizontal, and on both diagonals.</p>
<p>My initial thought was that this would be extremely computationally expensive. There are many permutations of selected squares which can lead to a win, and many which do not. For example, XX — — OOO — — XX with O to move will lead to a win for O, but with X to move will not. However, I convinced myself that any static evaluation which built towards a win would find winning nodes at sufficient depth, so finding extremely detailed evaluation was less important than a general approximation.</p>
<p>Building from this thought, I decided to count the number of 4-in-a-rows (4s) and give them a high score, along with the 3s and 2s. Each in-a-row would be given an exponentially increasing “reward”, so that 4s scores much higher than 2×2s. For example, the payout function might be f(n) = 2^N for 2, 3, and 4 so that f(4) = 16 and 2×f(2) = 8. This ensures the desired result, that the optimal configuration of N squares is Ns.</p>
<p>Eventually, I determined that it was sufficient to simply count 2s with overlaps, since allowing double counts would still favor longer sequences of squares, but would not require separate checks for each length. Therefore, if 2s was rewarded 1, then XXX would be rewarded 2, and XXXX would be rewarded 3. This means that 4s is still more the most efficient configuration of four squares, since XX — XX only evaluates to 2.</p>
<h2 id="gameover-function">Gameover function</h2>
<p>This function simply needs to return true if the game is over and a player has won. After the simplifications to the static evaluator, the gameover function behaves almost identically. Instead of counting 2s, we check for the presence of 5s.</p>
<h2 id="bitmasks">Bitmasks</h2>
<p>Here is where the fun begins. I realized that a very efficient way of representing a game board was with a sequence of bits, where 1 represented an occupied square, and 0 represented an unoccupied square. A game state would therefore only require a bit sequence for each player (the game engine would prevent overlapping bits). For a 15×15=225 square board, each player’s occupied squares could be represented with a number 225 bits in length. Although Javascript Numbers are only 53 bits long, Javascript has a newer primitive, BigInt, which can store numbers of arbitrary length.</p>
<p>The biggest benefit of representing the game board this way is that it facilitates bitwise operations, which drastically reduces the time complexity for the static evaluator and gameover functions.</p>
<figure><img src="/img/bitmask.gif"
			alt="Here the mask is shown in white, and the actual squares occupied are shown in orange. With each step in the bitmask check, the board and the mask are bitwise ANDed together, a very fast operation which reduces the computational complexity required in the static evaluator and gameover function."><figcaption>
			<p>Here the mask is shown in white, and the actual squares occupied are shown in orange. With each step in the bitmask check, the board and the mask are bitwise ANDed together, a very fast operation which reduces the computational complexity required in the static evaluator and gameover function.</p>
		</figcaption>
</figure>

<h2 id="about-bigint">About BigInt</h2>
<p>The BigInt primitive is a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt">newer built-in type</a> in Javascript, and as such is unsupported in some browsers. In particular, Internet Explorer and Safari do not have BigInt as a primitive. Although there are polyfills available for BigInt, they do not have the same performance as the native type. I decided that as a demonstration of the Minimax algorithm, supporting all browsers was not a priority.</p>
<h2 id="web-workers">Web Workers</h2>
<p>Most people know that Javascript is single threaded. It is, except when it isn’t. Web Workers are a way of multithreading in the browser, which in this context is pretty important because it helps to avoid freezing the user interface. In this game, the board state is handed off to a Web Worker thread, which computes the best move and returns it to the main thread. Progress is reported back periodically to the main thread as well, which is shown in a progress bar underneath the Gomoku2049 logo.</p>
<p>Theoretically, I could have taken further advantage of multithreading when creating this game. Each branch in the decision tree can be parallelized, allowing for simultaneous computation of each node’s value. For example, a new thread could be used to evaluate each of the AI’s possible moves. Unfortunately, the number of possible moves for the AI can be quite high later in the game, and browsers limit the number of Web Workers allowed (Chrome allows 60, Firefox allows 20, etc.) so instead of spawning a new worker for each top level branch, threads would need to be spawned from a shared thread pool.</p>
<p><a href="https://github.com/lukesalamone/gomoku-2049">The full source code for this game can be found here.</a></p>
]]></content>
        </item>
        
    </channel>
</rss>
