<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Risheekkumar Baskaran</title>
<link>https://risheekkumar.in/archive.html</link>
<atom:link href="https://risheekkumar.in/archive.xml" rel="self" type="application/rss+xml"/>
<description>Notes on machine learning, foundation models, recommender systems, and production AI.</description>
<generator>quarto-1.10.18</generator>
<lastBuildDate>Tue, 30 Dec 2025 18:30:00 GMT</lastBuildDate>
<item>
  <title>Programmatic Prompt Optimization: Replacing Intuition with Algorithms</title>
  <dc:creator>Risheek kumar B</dc:creator>
  <link>https://risheekkumar.in/posts/gepa-impact/gepa_impact_final.html</link>
  <description><![CDATA[ 




<script>
document.addEventListener("DOMContentLoaded", () => {
  const article = document.querySelector("#quarto-document-content");
  const output = document.querySelector(".post-reading-time-value");

  if (!article || !output) return;

  const copy = article.cloneNode(true);
  copy.querySelector("#title-block-header")?.remove();
  copy.querySelectorAll("pre, code, script, style").forEach((element) => element.remove());

  const words = copy.textContent.trim().match(/\S+/g)?.length ?? 0;
  const minutes = Math.max(1, Math.ceil(words / 200));
  output.textContent = minutes + " min read";
});
</script>


<p>I’d built plenty of successful POCs with LLM prompts. Impressive in demos, but when it came time to deploy at scale, precision wasn’t good enough… recall wasn’t high. I’d tried methods from many prompt tutorials from <a href="https://platform.openai.com/docs/guides/prompt-engineering">openai</a>, <a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview">claude</a> etc.</p>
<p>Manual prompt engineering is fundamentally trial and error. What works today might fail tomorrow and there’s no systematic way to improve it.</p>
<p>My task at hand was getting insights from unstructured customer interaction data. I’d spent three weeks fine-tuning a single prompt by hand, iterating through variations, hoping to stumble on something that worked consistently. It wasn’t cutting it. Even if this worked now, what about model upgrades? What about maintenance?</p>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Tip</span>The key observation
</div>
</div>
<div class="callout-body-container callout-body">
<p>LLMs are remarkably good at <em>giving feedback</em>. They can look at their own outputs and tell you exactly what went wrong. So why couldn’t they convert that feedback into better prompts automatically?</p>
</div>
</div>
<p>I searched for tools that could achieve this out of pure desperation. That’s when I stumbled upon <a href="https://thedataquarry.com/blog/learning-dspy-1-the-power-of-good-abstractions/">DSPy</a> and the concept of prompt optimizers: systems that treat prompt engineering as an optimization problem rather than an art. When <a href="https://dspy.ai/tutorials/gepa_aime/">GEPA</a> was released, I knew I had to test it.</p>
<p>Think of it like compilation: you write high-level code (your task definition), and the compiler transforms it into optimized machine instructions (a prompt that actually works). You don’t hand-tune assembly, so why hand-tune prompts?</p>
<section id="the-pilot" class="level2">
<h2 class="anchored" data-anchor-id="the-pilot">The Pilot</h2>
<p>I needed a simple test to see if GEPA could work for my use case. So I created a synthetic dataset of 27 sales call transcripts that represented a real challenge we face: <strong>detecting presence of required behaviors and predicting call quality (good/bad)</strong>. The transcripts were hand-labeled across 7 behavior categories <code>(introduction, needs, value proposition, objection handling, benefit reinforcement, risk reduction, and closing)</code>. Small enough to iterate fast, realistic enough to validate the approach—and representative of a problem I’d hit repeatedly: intent extraction and call evaluation look easy for a few cases, but precision and recall tank at scale.</p>
<p>I expected weeks of iteration. Instead, I got meaningful results in a single run. Usually a show piece like this would be carefully selected sample to show the power of the approach. Here this is literally first attempt, that in itself tells the power of the approach.</p>
<div id="fc5bed8c" class="cell" data-time_run="2025-12-31T10:47:07.998250+00:00" data-execution_count="9">
<div class="cell-output cell-output-display" data-execution_count="9">
<div style="max-width:1200px; margin:0 auto; padding:0 16px;">
  <div style="display:flex; flex-wrap:wrap; gap:20px; font-family:system-ui,-apple-system,sans-serif;">
    <div style="flex:1 1 300px; background:#f8f9fa; border-radius:12px; padding:20px; border:1px solid #e0e0e0;">
      <h3 style="margin:0 0 12px 0; color:#1a73e8; font-size:14px; text-transform:uppercase; letter-spacing:1px;" class="anchored">📞 Input: Call Transcript</h3>
      <div style="background:white; padding:16px; border-radius:8px; font-size:13px; line-height:1.6; max-height:300px; overflow-y:auto; white-space:pre-wrap; color:#333;">agent: Hi, good afternoon! This is Maya calling from Citi Corp. Am I speaking with Jordan Lee?

customer: Yes, this is Jordan.

agent: Great, Jordan. How are you doing today?

customer: I'm good, thanks. Busy afternoon, but I have a few minutes.

agent: I appreciate you taking the time...</div>
    </div>
    <div style="flex:1 1 300px; background:#f0f7f0; border-radius:12px; padding:20px; border:1px solid #c8e6c9;">
      <h3 style="margin:0 0 12px 0; color:#2e7d32; font-size:14px; text-transform:uppercase; letter-spacing:1px;" class="anchored">📊 Output: Analysis</h3>
      <div style="background:white; padding:16px; border-radius:8px; margin-bottom:12px;">
        <div style="font-size:12px; color:#666; margin-bottom:4px;">Call Quality</div>
        <div style="font-size:24px; font-weight:600; color:#2e7d32;">✓ Good</div>
      </div>
      <div style="background:white; padding:16px; border-radius:8px;">
        <div style="font-size:12px; color:#666; margin-bottom:8px;">Detected Categories</div>
        <div style="display:flex; flex-wrap:wrap; gap:6px;">
          <span style="background:#e8f5e9; color:#2e7d32; padding:4px 10px; border-radius:16px; font-size:12px;">Introduction/Rapport ✓</span>
          <span style="background:#e8f5e9; color:#2e7d32; padding:4px 10px; border-radius:16px; font-size:12px;">Need Assessment ✓</span>
          <span style="background:#ffebee; color:#c62828; padding:4px 10px; border-radius:16px; font-size:12px;">Value Proposition ✗</span>
          <span style="background:#e8f5e9; color:#2e7d32; padding:4px 10px; border-radius:16px; font-size:12px;">Objection Handling ✓</span>
          <span style="background:#e8f5e9; color:#2e7d32; padding:4px 10px; border-radius:16px; font-size:12px;">Benefit Reinforcement ✓</span>
          <span style="background:#e8f5e9; color:#2e7d32; padding:4px 10px; border-radius:16px; font-size:12px;">Risk Reduction ✓</span>
          <span style="background:#e8f5e9; color:#2e7d32; padding:4px 10px; border-radius:16px; font-size:12px;">Call to Action ✓</span>
        </div>
      </div>
    </div>
  </div>
</div>
</div>
</div>
<section id="results" class="level3">
<h3 class="anchored" data-anchor-id="results">Results</h3>
<table class="caption-top table">
<colgroup>
<col style="width: 31%">
<col style="width: 18%">
<col style="width: 18%">
<col style="width: 31%">
</colgroup>
<thead>
<tr class="header">
<th>Approach</th>
<th>Cost</th>
<th>Time</th>
<th>Accuracy</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Manual prompt engineering</td>
<td>$100-1000 (engineer time)</td>
<td>Days to weeks</td>
<td>72%</td>
</tr>
<tr class="even">
<td><strong>GEPA</strong></td>
<td>~$2</td>
<td>10 hours</td>
<td>81%</td>
</tr>
<tr class="odd">
<td>GEPA with error analysis</td>
<td>~$0.5</td>
<td>3 hours</td>
<td>90%</td>
</tr>
</tbody>
</table>
<p>The optimizer ran for about 10 hours, cost roughly $2, and explored over 200 prompt variants. Through genetic mutation and Pareto selection, it whittled those down to 9 “survivors”—prompts that excelled at different subsets of the problem. The best performer jumped from 72% to 81% accuracy, a lift I hadn’t achieved in months of manual tuning.</p>
<p>The intermediate prompts evolving caught my eye. I could see the optimizer discovering nuances I’d never thought to include: explicit definitions for each category, step-by-step rules for edge cases, domain-specific guidance about soft pulls versus hard pulls. The quality of the reasoning it produced while iterating was genuinely impressive.</p>
<p>Hopefully I have convinced you that this method is powerful, lets see how i did it and you can follow similar steps for yours as well.</p>
</section>
</section>
<section id="detailed-implementation-of-gepa" class="level2">
<h2 class="anchored" data-anchor-id="detailed-implementation-of-gepa">Detailed Implementation of GEPA</h2>
<p>Let’s first understand what GEPA does, then dive into code for this specific usecase. If you want deeper dive on how GEPA works, i have previously written a detailed piece here: <a href="https://risheekkumar.in/posts/gepa-deepdive/gepa_final_article.html">GEPA</a></p>
<p>Code-first folks: here’s the notebook: <a href="https://github.com/risheekkumarb/gepa-impact-article/blob/main/exploration%20v2.ipynb">github link</a></p>
<blockquote class="blockquote">
<p>We’ll use DSPy to run GEPA. If you’re new to DSPy, it’s a framework that treats prompts as code you can optimize programmatically. For background, see <a href="https://thedataquarry.com/blog/learning-dspy-3-working-with-optimizers/">The Data Quarry’s guide</a>.</p>
</blockquote>
<hr>
<section id="how-gepa-works" class="level3">
<h3 class="anchored" data-anchor-id="how-gepa-works">How GEPA Works</h3>
<p>GEPA (Genetic-Pareto Algorithm) differs from traditional optimization in three key ways:</p>
<ol type="1">
<li><p><strong>Reflective Mutation</strong>: The LLM <em>reads failure feedback</em> and proposes targeted improvements. It’s not random guessing—it’s reasoning about what went wrong.</p></li>
<li><p><strong>Pareto Selection</strong>: Instead of keeping only the single best prompt, GEPA maintains a “frontier” of diverse specialists. One prompt might excel at detecting objection handling; another at predicting outcomes. This prevents catastrophic forgetting.</p></li>
<li><p><strong>Text-as-Feedback</strong>: Traditional RL uses scalar rewards. GEPA exploits rich textual feedback (“You incorrectly marked this as rapport-building because…”) to guide mutations precisely.</p></li>
</ol>
<hr>
</section>
<section id="prerequisites" class="level3">
<h3 class="anchored" data-anchor-id="prerequisites">Prerequisites</h3>
<p>To Use GEPA, we need 3 components.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 26%">
<col style="width: 34%">
<col style="width: 39%">
</colgroup>
<thead>
<tr class="header">
<th>Component</th>
<th>What it does</th>
<th>Why it matters</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>DSPy Signature</strong></td>
<td>Your baseline prompt defining the task</td>
<td>The “prompt” being optimized</td>
</tr>
<tr class="even">
<td><strong>Metric &amp; Feedback</strong></td>
<td>Returns score + textual feedback</td>
<td>Tells optimizer what “good” looks like <em>and why</em></td>
</tr>
<tr class="odd">
<td><strong>Dataset</strong></td>
<td>Labeled examples (train/val/test)</td>
<td>Ground truth for evaluation</td>
</tr>
</tbody>
</table>
<hr>
<section id="the-dspy-signature" class="level4">
<h4 class="anchored" data-anchor-id="the-dspy-signature">The DSPy Signature</h4>
<p>In DSPy, A Signature defines input/output schema; the instructions in the docstring become part of the prompt. My initial prompt was embarrassingly simple—just two lines:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> CallAnalysis(dspy.Signature):</span>
<span id="cb1-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb1-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    Read the provided call transcript and analyze it comprehensively.</span></span>
<span id="cb1-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    Determine both: (1) which categories the agent displayed, and </span></span>
<span id="cb1-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    (2) whether the call will lead to conversion or customer retention.</span></span>
<span id="cb1-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    """</span></span>
<span id="cb1-7">    message: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dspy.InputField()</span>
<span id="cb1-8">    categories: List[Literal[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"introduction_rapport_building"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"need_assessment_qualification"</span>, </span>
<span id="cb1-9">                             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"value_proposition_feature_mapping"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"objection_handling"</span>, </span>
<span id="cb1-10">                             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"benefit_reinforcement"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"risk_reduction_trust_building"</span>, </span>
<span id="cb1-11">                             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"call_to_action_closing"</span>]] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dspy.OutputField()</span>
<span id="cb1-12">    final_result: Literal[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'good'</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'bad'</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dspy.OutputField()</span>
<span id="cb1-13"></span>
<span id="cb1-14">program <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dspy.ChainOfThought(CallAnalysis)</span></code></pre></div></div>
<hr>
</section>
<section id="metric-function" class="level4">
<h4 class="anchored" data-anchor-id="metric-function">Metric Function</h4>
<p>A Metric tells us whether we are moving in the right direction. In this case, accuracy of categories detected and final prediction that whether call was good or bad - both were important. Hence metric will be mean of both the entities</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> call_qual_metric(gold, pred):</span>
<span id="cb2-2">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> gold <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> pred <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span></span>
<span id="cb2-3"></span>
<span id="cb2-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> category_qual_metric(gold, pred):</span>
<span id="cb2-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Compute score for categories using set operations."""</span></span>
<span id="cb2-6">    pred_set <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>(pred)</span>
<span id="cb2-7">    gold_true <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {k <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> k, v <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> gold.items() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> v}</span>
<span id="cb2-8">    gold_false <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {k <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> k, v <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> gold.items() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> v}</span>
<span id="cb2-9">    </span>
<span id="cb2-10">    correct <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(gold_true <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> pred_set) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(gold_false <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pred_set)</span>
<span id="cb2-11">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> correct <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(gold)</span>
<span id="cb2-12"></span>
<span id="cb2-13"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> comb_metric(gold, pred, trace<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, pred_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, pred_trace<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>):</span>
<span id="cb2-14">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Overall metric combining both scores."""</span></span>
<span id="cb2-15">    call_qual <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> call_qual_metric(gold.final_result, pred.final_result)</span>
<span id="cb2-16">    category_qual <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> category_qual_metric(gold.categories, pred.categories)</span>
<span id="cb2-17">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (call_qual <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> category_qual) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span></span></code></pre></div></div>
<hr>
</section>
<section id="adding-feedback" class="level4">
<h4 class="anchored">Adding Feedback</h4>
<p>This is the key enabler. A basic metric only returns a score but not <em>why</em> it failed. With feedback, the optimizer can reason about failures and propose targeted fixes.</p>
<div id="535ea46d" class="cell" data-hide_input="true" data-time_run="2025-12-31T10:21:01.090786+00:00">
<div class="cell-output cell-output-display" data-execution_count="0">

<style>
  .compare-container { display: flex; gap: 20px; flex-wrap: wrap; }
  .compare-box { flex: 1; min-width: 280px; border: 2px solid #ccc; padding: 15px; border-radius: 8px; }
  .compare-box.basic { background: #f9f9f9; border-color: #ccc; }
  .compare-box.feedback { background: #f0fff0; border-color: #27ae60; }
  .compare-box h3 { margin-top: 0; }
  .compare-box pre { white-space: pre-wrap; font-size: 11px; overflow-x: auto; }
  @media (max-width: 600px) {
    .compare-container { flex-direction: column; }
    .compare-box { min-width: 100%; }
  }
</style>
<div class="compare-container">
  <div class="compare-box basic">
    <h3 style="color: #d35400;" class="anchored" data-anchor-id="adding-feedback">❌ Metric (Score Only)</h3>
    <pre>def comb_metric(example, pred):
    gold_cat = json.loads(example['answer'])
    gold_final = example['final_result']

    # Category score
    correct = sum(1 for k, v in gold_cat.items() 
                  if (v and k in pred.categories) or 
                     (not v and k not in pred.categories))
    cat_score = correct / len(gold_cat)

    # Final result score
    final_score = 1.0 if gold_final == pred.final_result else 0.0

    return (cat_score + final_score) / 2</pre>
    <br>
    <p><b>Problem:</b> Optimizer only knows "0.7" — no idea <i>why</i> it failed.</p>
  </div>
  <div class="compare-box feedback">
    <h3 style="color: #27ae60;" class="anchored">✅ Metric with Feedback</h3>
    <pre>def comb_metric_with_feedback(example, pred, pred_name=None):
    # ... same scoring logic as above ...

    # Generate textual feedback
    if gold_final != pred.final_result:
        fb = f"Incorrect: predicted {pred.final_result}, actual {gold_final}"
    else:
        fb = f"Correct: {gold_final}"

    if incorrectly_included:
        fb += f"\nFalse positives: {incorrectly_included}"
    if incorrectly_excluded:
        fb += f"\nMissed categories: {incorrectly_excluded}"

    return dspy.Prediction(score=score, feedback=fb)</pre>
    <br>
    <p><b>Benefit:</b> Optimizer sees "Missed: intro_rapport" → can propose targeted fix.</p>
  </div>
</div>
</div>
</div>
<p><strong>Code example:</strong></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> call_qual_feedback(gold, pred):</span>
<span id="cb3-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">""" Generate feedback for final result module. """</span></span>
<span id="cb3-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> gold <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> pred:</span>
<span id="cb3-4">        fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"You correctly classified the sales call as `</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>gold<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">`. This sales call is indeed `</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>gold<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">`."</span></span>
<span id="cb3-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span>:</span>
<span id="cb3-6">        fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"You incorrectly classified the sales call as `</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>pred<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">`. The correct sales call is `</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>gold<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">`. Think about how you could have reasoned to get the correct sales call label."</span></span>
<span id="cb3-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> fb</span>
<span id="cb3-8"></span>
<span id="cb3-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> category_qual_feedback(gold, pred):</span>
<span id="cb3-10">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Generate feedback using set operations."""</span></span>
<span id="cb3-11">    pred_set <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>(pred)</span>
<span id="cb3-12">    gold_true <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {k <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> k, v <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> gold.items() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> v}</span>
<span id="cb3-13">    gold_false <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {k <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> k, v <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> gold.items() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> v}</span>
<span id="cb3-14">    </span>
<span id="cb3-15">    correctly_included <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> gold_true <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> pred_set</span>
<span id="cb3-16">    incorrectly_included <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> gold_false <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> pred_set</span>
<span id="cb3-17">    incorrectly_excluded <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> gold_true <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pred_set</span>
<span id="cb3-18">    correctly_excluded <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> gold_false <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pred_set</span>
<span id="cb3-19">    </span>
<span id="cb3-20">    score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(correctly_included) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(correctly_excluded)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(gold)</span>
<span id="cb3-21">    </span>
<span id="cb3-22">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>:</span>
<span id="cb3-23">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Perfect. Correctly identified: `</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>correctly_included<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">`."</span>, score</span>
<span id="cb3-24">    </span>
<span id="cb3-25">    fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Correctly identified: `</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>correctly_included<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">`.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-26">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> incorrectly_included:</span>
<span id="cb3-27">        fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"False positives: `</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>incorrectly_included<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">`.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-28">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> incorrectly_excluded:</span>
<span id="cb3-29">        fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Missed: `</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>incorrectly_excluded<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">`.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-30">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> fb</span>
<span id="cb3-31"></span>
<span id="cb3-32"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> comb_metric_with_feedback(gold, pred, trace<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, pred_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, pred_trace<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>):</span>
<span id="cb3-33">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-34"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    Computes a score and provides feedback for the call analysis prediction.</span></span>
<span id="cb3-35"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    Returns total score if pred_name is None, otherwise returns dspy.Prediction with score and feedback.</span></span>
<span id="cb3-36"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    """</span></span>
<span id="cb3-37">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Compute feedback and scores</span></span>
<span id="cb3-38">    cal_fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> call_qual_feedback(gold.final_result, pred.final_result)</span>
<span id="cb3-39">    cat_fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> category_qual_feedback(gold.categories, pred.categories)</span>
<span id="cb3-40">    fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> cal_fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> cat_fb</span>
<span id="cb3-41">    score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> comb_metric(gold, pred)</span>
<span id="cb3-42">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> dspy.Prediction(score<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>score, feedback<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>fb)</span></code></pre></div></div>
<hr>
</section>
</section>
<section id="running-gepa" class="level3">
<h3 class="anchored" data-anchor-id="running-gepa">Running GEPA</h3>
<p>With prerequisites in place, optimization is straightforward:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> dspy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> GEPA</span>
<span id="cb4-2"></span>
<span id="cb4-3">optimizer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> GEPA(</span>
<span id="cb4-4">    metric<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>comb_metric_with_feedback,</span>
<span id="cb4-5">    auto<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"light"</span>,</span>
<span id="cb4-6">)</span>
<span id="cb4-7"></span>
<span id="cb4-8">optimized_program <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> optimizer.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">compile</span>(program, trainset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>tset, valset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>vset)</span></code></pre></div></div>
<hr>
</section>
<section id="post-gepa-run" class="level3">
<h3 class="anchored" data-anchor-id="post-gepa-run">Post GEPA run</h3>
<p>GEPA ran for 10 hrs on my PC. I saw the 2-line prompt evolve into ~1,500 words of instruction discovering nuances like “a bare greeting isn’t rapport-building; look for warmth and time acknowledgment”.</p>
<div class="callout callout-style-simple callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Note</span>Pay Attention
</div>
</div>
<div class="callout-body-container callout-body">
<p>In some cases, it came up with better instructions and heuristics than me. Kindah felt fearful yet exciting.</p>
</div>
</div>
<div id="e43eb8ba" class="cell" data-hide_input="true" data-time_run="2025-12-31T10:22:24.379298+00:00">
<div class="cell-output cell-output-display" data-execution_count="0">

<div style="display: flex; flex-wrap: wrap; gap: 20px;">
  <div style="flex: 1 1 300px; min-width: 280px; border: 2px solid #ccc; padding: 15px; border-radius: 8px; background: #f9f9f9;">
    <h3 style="color: #d35400; margin-top: 0;" class="anchored">Initial Prompt</h3>
    <pre style="white-space: pre-wrap; font-size: 12px;">Read the provided call transcript and analyze it comprehensively.
Determine both: (1) which categories the agent displayed, and (2) whether the call will lead to conversion or customer retention.</pre>
  </div>
  <div style="flex: 1 1 300px; min-width: 280px; border: 2px solid #27ae60; padding: 15px; border-radius: 8px; background: #f0fff0;">
    <h3 style="color: #27ae60; margin-top: 0;" class="anchored">Optimized Prompt (GEPA)</h3>
    <pre style="white-space: pre-wrap; font-size: 11px; max-height: 400px; overflow-y: auto;">New Instructions for Analyzing Banking/Card Transaction Call Transcripts

Overview
You are an analysis assistant whose job is to evaluate sales/transact‑ion-focused call transcripts in the banking/credit-card domain. For each transcript, produce a compact, structured analysis with two main objectives:
  (a) identify the agent behavior categories demonstrated (from the seven pillars below), and
  (b) judge whether the call outcome is good or bad.

Inputs you will receive
- A complete transcript of a single call between an agent and a customer. Transcripts may include labels such as "agent:" and "customer:" and may cover topics like card offers, fees, rewards, security, and next steps.

What you must produce (three sections exactly)
1) reasoning
   - Provide a concise, bullets-style justification for every pillar category you detected in the transcript.
   - Include short quotes or paraphrases from the transcript to illustrate why the category applies. Do not introduce facts or assumptions beyond what is in the transcript.
   - If you detect a strength/weakness signal about the outcome, include a brief, one- to two-sentence note here describing how strong the signal is and what would push it toward conversion or toward retention.
   - This section may contain a small, optional note about outcome strength, but must not introduce information outside the transcript.

2) categories
   - Output a Python-like list of the detected pillar categories in the exact order they first appeared in the transcript.
   - Example format: ['introduction_rapport_building', 'need_assessment_qualification', ...]

3) final_result
   - A single word indicating the call outcome:
     - good — the call demonstrates strong agent performance and is likely to lead to conversion or retention.
     - bad — the call shows weak agent performance or missed opportunities.
   - Do not add any qualifiers in this field; use exactly one of the two keywords above.

Optional but encouraged: assess the strength of the outcome
- If you include it (recommendation), place this assessment only in reasoning as the optional strength_of_outcome note. Keep it concise (one or two sentences). It should address:
  - How strong is the good/bad signal?
  - What would most likely push the outcome toward good or toward bad?

Pillar definitions (seven bank/card-specific categories)
- introduction_rapport_building
  - Includes opening greetings, courtesy, acknowledgment of time, and attempts to establish rapport.
  - Examples: greetings, confirming time, polite introductions, small talk about fit or time constraints.
- need_assessment_qualification
  - Involves asking about customer needs, usage, spend patterns, eligibility checks, and whether the product fits (e.g., business vs personal, employee cards, annual fees, soft vs hard pulls).
- value_proposition_feature_mapping
  - Linking card features to tangible, customer-relevant benefits (rewards, protections, credits) and showing how those features align with stated needs.
- objection_handling
  - Addressing concerns about price, complexity, trust, enrollment, or process obstacles. Includes acknowledging concerns and offering clarifications or mitigations.
- benefit_reinforcement
  - Reiterating concrete benefits and value after objections or hesitations, often tying back to the customer's stated needs.
- risk_reduction_trust_building
  - Providing security assurances, privacy protections, non-hard-pull options, guarantees, terms clarity, or brand trust signals.
- call_to_action_closing
  - Concrete next steps or commitments: soft checks, secure links, email/mail options, scheduling follow-ups, or instructions to apply/get more information.

How to apply the rules
- For every transcript, read from start to finish. Mark each pillar as soon as its criteria are clearly demonstrated.
- If a single utterance clearly satisfies more than one pillar, count it under all applicable pillars.
- If a pillar is not clearly demonstrated anywhere in the transcript, do not include it in the categories list.
- Record the detected pillars in the exact order of their first appearance in the transcript.
- The final_result should reflect the overall trajectory of the call as described above.

Output constraints and format
- Do not introduce any facts not present in the transcript.
- Do not insert subjective opinions beyond what is grounded in the transcript.
- Use the exact section headings and formatting:
  reasoning
  categories
  final_result
- Do not include extraneous content beyond the three sections above.

Domain-specific considerations
- You may encounter references to soft pulls vs hard pulls, online applications, secure links, email follow-ups, or scheduled follow-ups. Treat these as legitimate "call_to_action_closing" or "risk_reduction_trust_building" elements as appropriate.
- When quoting or paraphrasing, keep quotes brief and focused on the reason for the pillar.
- If PII appears in the transcript (e.g., partial SSN or addresses), quote minimally and do not reveal full sensitive data in your justification. You may paraphrase or reference the presence of sensitive data without reproducing it.

Example behavior (not to reproduce here)
- A transcript with strong agent behaviors and clear next steps is more likely good; a transcript with missed opportunities, customer hesitation, or weak closing is more likely bad.

End result
- Return exactly three sections for every transcript analyzed, with the content governed by the rules above. This format enables consistent, comparable, and transparent analysis across transcripts.</pre>
  </div>
</div>
</div>
</div>
<p>Result: <strong>72% → 81% accuracy</strong>. More importantly, the process was repeatable.</p>
<p>For a deeper technical dive: <a href="https://risheekkumar.in/posts/gepa-deepdive/gepa_final_article.html">GEPA Deepdive</a></p>
</section>
</section>
<section id="breaking-through-the-81-ceiling-with-error-analysis" class="level2">
<h2 class="anchored" data-anchor-id="breaking-through-the-81-ceiling-with-error-analysis">Breaking Through the 81% Ceiling with Error Analysis</h2>
<p>With the above proof, I deployed it internally and it worked reliably wherever it was implemented properly. But I wasn’t satisfied. When I manually reviewed the failing cases, something bothered me: these weren’t hard examples. Given a hint, the LLM could easily get them right. So why was it failing?</p>
<p>So I dug in and exported the misclassified examples to a spreadsheet and studied them systematically:</p>
<div style="overflow-x: auto;">
<table class="caption-top table">
<colgroup>
<col style="width: 25%">
<col style="width: 25%">
<col style="width: 25%">
<col style="width: 25%">
</colgroup>
<thead>
<tr class="header">
<th>Input (truncated)</th>
<th>Actual</th>
<th>Predicted</th>
<th>What Went Wrong</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Tyler calling from dominos… “sounds good, I’ll send that link…”</td>
<td>bad</td>
<td>good</td>
<td>Customer showed hesitation (“I’m really not sure…”) but agent rushed to close</td>
</tr>
<tr class="even">
<td>Mark from JP calling about credit solutions…</td>
<td>rapport = <strong>false</strong></td>
<td>rapport = <strong>true</strong></td>
<td>Agent said “Hi, this is Mark from JP” with no warmth or rapport signals</td>
</tr>
</tbody>
</table>
</div>
<p><strong>The pattern became clear:</strong> the model was over-detecting <code>introduction_rapport_building</code>—treating any greeting as rapport. It also sometimes marked calls as “good” just because next steps existed, even when the customer showed clear hesitation.</p>
<style>
  .flow-container { 
    display: flex; 
    flex-direction: row; /* Force horizontal by default */
    flex-wrap: nowrap; /* Keep it on one line for desktop */
    gap: 12px; 
    justify-content: center; 
    align-items: center; 
    font-family: system-ui, sans-serif; 
    padding: 20px 0; 
  }
  .flow-box { 
    background: #f8f9fa; 
    border: 2px solid #ddd; 
    border-radius: 10px; 
    padding: 16px 20px; 
    text-align: center; 
    flex: 1; /* Allows boxes to share space equally */
    max-width: 180px; 
  }
  .flow-box h4 { margin: 0 0 6px 0; font-size: 14px; color: #333; }
  .flow-box p { margin: 0; font-size: 12px; color: #666; }
  .flow-box.start { border-color: #1a73e8; background: #e8f0fe; }
  .flow-box.process { border-color: #f9ab00; background: #fef7e0; }
  .flow-box.result { border-color: #34a853; background: #e6f4ea; }
  .flow-arrow { font-size: 24px; color: #999; flex-shrink: 0; }

  /* Responsive: Switch to vertical at 800px or less */
  @media (max-width: 800px) {
    .flow-container { flex-direction: column; }
    .flow-arrow { transform: rotate(90deg); }
    .flow-box { max-width: 100%; width: 200px; } /* Fixed width looks better in vertical stacks */
  }
</style>
<div class="flow-container">
<div class="flow-box start">
<h4 class="anchored">
🎯 GEPA Run 1
</h4>
<p>
72% → 81%
</p>
</div>
<span class="flow-arrow">→</span>
<div class="flow-box process">
<h4 class="anchored">
🔍 Error Analysis
</h4>
<p>
Export failures, find patterns
</p>
</div>
<span class="flow-arrow">→</span>
<div class="flow-box process">
<h4 class="anchored">
✏️ Add Feedback
</h4>
<p>
Targeted hints for failures
</p>
</div>
<span class="flow-arrow">→</span>
<div class="flow-box result">
<h4 class="anchored">
🚀 GEPA Run 2
</h4>
<p>
81% → 90%
</p>
</div>
</div>
<section id="the-solution-targeted-feedback-in-the-metric" class="level3">
<h3 class="anchored" data-anchor-id="the-solution-targeted-feedback-in-the-metric">The Solution: Targeted Feedback in the Metric</h3>
<p>My hunch was simple: if I could teach the optimizer <em>why</em> these specific cases failed, it could learn the distinctions. I added a <code>feedback</code> column to my dataset and filled it only for mistagged cases—writing out precisely why my label was correct. For example:</p>
<blockquote class="blockquote">
<p><em>“The agent said ‘Hi, this is Mark from JP’ without warmth, time acknowledgment, or rapport-building. A bare introduction doesn’t qualify as introduction_rapport_building.”</em></p>
</blockquote>
<style>
  .iter-table { border-collapse: collapse; font-family: system-ui, sans-serif; font-size: 13px; }
  .iter-table th, .iter-table td { padding: 10px 14px; border: 1px solid #ddd; text-align: center; }
  .iter-table th { background: #f5f5f5; font-weight: 600; }
  .iter-table .example { text-align: left; font-weight: 500; }
  .iter-table .pass { background: #c8e6c9; color: #2e7d32; }
  .iter-table .fail { background: #ffcdd2; color: #c62828; }
  .iter-table .fb { background: #fff8e1; font-size: 11px; text-align: left; max-width: 180px; }
  .iter-table .na { color: #999; }
</style>
<div style="overflow-x: auto;">
<table class="iter-table">
<tbody><tr>
<th>
Example
</th>
<th>
Iter 1
</th>
<th>
Feedback
</th>
<th>
Iter 2
</th>
<th>
Feedback
</th>
<th>
Iter 3
</th>
</tr>
<tr>
<td class="example">
Tyler
</td>
<td class="fail">
❌
</td>
<td class="fb">
Look for hesitation signals
</td>
<td class="pass">
✅
</td>
<td class="na">
—
</td>
<td class="na">
—
</td>
</tr>
<tr>
<td class="example">
Mark
</td>
<td class="fail">
❌
</td>
<td class="fb">
Greeting ≠ rapport
</td>
<td class="fail">
❌
</td>
<td class="fb">
Need warmth/courtesy
</td>
<td class="pass">
✅
</td>
</tr>
<tr>
<td class="example">
John
</td>
<td class="fail">
❌
</td>
<td class="fb">
Greeting ≠ rapport
</td>
<td class="pass">
✅
</td>
<td class="na">
—
</td>
<td class="na">
—
</td>
</tr>
</tbody></table>
</div>
<p>Then I passed this feedback directly to GEPA through the metric function:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> comb_metric_with_feedback(gold, pred, trace<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, pred_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, pred_trace<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>):</span>
<span id="cb5-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Metric that returns score + feedback, including targeted hints for known failure cases."""</span></span>
<span id="cb5-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Compute scores</span></span>
<span id="cb5-4">    call_qual <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> call_qual_metric(gold.final_result, pred.final_result)</span>
<span id="cb5-5">    category_qual <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> category_qual_metric(gold.categories, pred.categories)</span>
<span id="cb5-6">    score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (call_qual <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> category_qual) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span></span>
<span id="cb5-7">    </span>
<span id="cb5-8">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Generate base feedback</span></span>
<span id="cb5-9">    cal_fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> call_qual_feedback(gold.final_result, pred.final_result)</span>
<span id="cb5-10">    cat_fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> category_qual_feedback(gold.categories, pred.categories)</span>
<span id="cb5-11">    fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> cal_fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> cat_fb</span>
<span id="cb5-12"></span>
<span id="cb5-13">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Append targeted feedback from the dataset (if present)</span></span>
<span id="cb5-14">    fb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> gold.feedback</span>
<span id="cb5-15"></span>
<span id="cb5-16">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> dspy.Prediction(score<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>score, feedback<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>fb)</span></code></pre></div></div>
<p>Now when GEPA’s reflective mutations analyze failures, they see specific guidance like “look for warmth signals, not just greetings” instead of generic “wrong answer” feedback.</p>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Tip</span>💡 Pro tip:
</div>
</div>
<div class="callout-body-container callout-body">
<p>You could use an LLM to generate this feedback automatically, but doing it manually gives you fine-grained control over how the final prompt evolves. The LLM might miss the exact nuance you care about. In short, you’re writing the prompt <b>through</b> feedback.</p>
</div>
</div>
</section>
<section id="the-result" class="level3">
<h3 class="anchored" data-anchor-id="the-result">The Result</h3>
<p><strong>81% → 90% accuracy</strong> in just 3 hours and ~$0.50.</p>
<p>The key insight: GEPA’s genetic mutations work best when they have precise feedback to reason about. Generic “wrong answer” feedback produces generic improvements. Targeted feedback like “you’re conflating greetings with rapport” produces targeted fixes.</p>
</section>
</section>
<section id="lessons-learned-recommended-workflow" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learned-recommended-workflow">Lessons Learned &amp; Recommended Workflow</h2>
<p>The biggest lesson wasn’t technical, it was psychological. I stopped thinking of prompts as things I <em>write</em> and started thinking of them as things I <em>evolve</em>. My job shifted from “craft the perfect prompt” to “define what good looks like and let the system find it.”</p>
<section id="when-not-to-use-gepa" class="level3">
<h3 class="anchored" data-anchor-id="when-not-to-use-gepa">When NOT to Use GEPA</h3>
<p>GEPA only works when you can clearly define “correct.” If you can’t reliably label examples yourself or if the target is fuzzy or subjective, the optimizer will chase noise. I tried it on a sentiment task where human annotators disagreed 30% of the time. The results were inconsistent. <strong>Rule: if you can’t do it clearly as a human, don’t expect GEPA to figure it out.</strong></p>
</section>
<section id="my-recommended-workflow" class="level3">
<h3 class="anchored" data-anchor-id="my-recommended-workflow">My Recommended Workflow</h3>
<ol type="1">
<li><p><strong>Start with a clear metric and a personally-validated dataset.</strong> 20-50 examples you’ve labeled yourself, understanding the edge cases.</p></li>
<li><p><strong>Run GEPA → Review failures → Add targeted feedback.</strong> Study misclassified examples. Write <em>specific</em> explanations of why your label is correct.</p></li>
<li><p><strong>Run GEPA again → Stop when acceptable → Deploy.</strong> One or two iterations usually suffice. Don’t chase perfection.</p></li>
<li><p><strong>Post-deployment: monitor, collect failures, iterate.</strong> Production surfaces edge cases. Add them with feedback and re-run as needed.</p></li>
</ol>
<hr>
<p>The complete Jupyter notebook: <a href="https://github.com/risheekkumarb/gepa-impact-article/blob/main/exploration%20v2.ipynb">github link</a></p>


</section>
</section>

 ]]></description>
  <category>code</category>
  <category>research paper</category>
  <category>impact</category>
  <guid>https://risheekkumar.in/posts/gepa-impact/gepa_impact_final.html</guid>
  <pubDate>Tue, 30 Dec 2025 18:30:00 GMT</pubDate>
  <media:content url="https://risheekkumar.in/posts/gepa-impact/gepa_impact.webp" medium="image" type="image/webp"/>
</item>
<item>
  <title>GEPA Deepdive</title>
  <dc:creator>Risheek kumar B</dc:creator>
  <link>https://risheekkumar.in/posts/gepa-deepdive/gepa_final_article.html</link>
  <description><![CDATA[ 




<script>
document.addEventListener("DOMContentLoaded", () => {
  const article = document.querySelector("#quarto-document-content");
  const output = document.querySelector(".post-reading-time-value");

  if (!article || !output) return;

  const copy = article.cloneNode(true);
  copy.querySelector("#title-block-header")?.remove();
  copy.querySelectorAll("pre, code, script, style").forEach((element) => element.remove());

  const words = copy.textContent.trim().match(/\S+/g)?.length ?? 0;
  const minutes = Math.max(1, Math.ceil(words / 200));
  output.textContent = minutes + " min read";
});
</script>


<blockquote class="blockquote">
<p>This article was made possible by the research papers referenced throughout, the <a href="https://www.youtube.com/watch?v=fREQrxhBSk0">Weaviate discussion with Lakshya A Agrawal</a>, and <a href="https://www.answer.ai/posts/2025-10-13-video-to-doc.html">guidance</a> from Kerem Turgutlu at answer.ai. Created using the <a href="https://solveit.fast.ai">solveit</a> platform by fast.ai.</p>
</blockquote>
<p>You’ve spent hours tuning your agentic pipeline. The system prompt is 500 words of carefully crafted instructions. It works… 70% of the time. You have 50 labeled examples. Now what?</p>
<p>Fine-tuning requires thousands of samples. Reinforcement learning needs expensive rollout collection. Manual prompt iteration doesn’t scale. You’re stuck.</p>
<p>But what if you could match RL’s optimization performance using 50 examples instead of 5,000?</p>
<p><strong><a href="https://arxiv.org/abs/2507.19457">GEPA</a></strong> (Genetic-Pareto) does exactly this—by exploiting something traditional optimization ignores: the detailed textual traces that LLM systems already produce. Instead of reducing a complex trajectory to a single scalar reward, GEPA lets the LLM <em>reflect on its own failures</em> and propose improvements directly.</p>
<p>In this post, we’ll unpack how it works, why modern LLMs’ improving self-reflection capabilities make this approach newly viable, and what it means for practitioners building compound AI systems.</p>
<section id="the-problem-scalar-rewards-and-expensive-rollouts" class="level3">
<h3 class="anchored" data-anchor-id="the-problem-scalar-rewards-and-expensive-rollouts">The Problem: Scalar Rewards and Expensive Rollouts</h3>
<p>Traditional AI optimization techniques—reinforcement learning and fine-tuning—have achieved str results in domains with abundant data or cheap rollouts (complete execution traces from input to output). But what happens when evaluation is expensive?</p>
<p>Consider:</p>
<ul>
<li><strong>Agentic pipelines</strong> that invoke simulations, query rate-limited APIs, or run multi-step tool chains</li>
<li><strong>Code generation for novel hardware</strong>, where each evaluation requires compiling for custom silicon and executing on the device</li>
<li><strong>Complex reasoning tasks</strong> with expensive verification steps</li>
</ul>
<p>Collecting thousands of rollouts simply isn’t feasible in these settings.</p>
<p>Why? <strong>RL learns by comparison</strong>. A 500-step trajectory collapses to <code>reward = 0.73</code>. Did step 12 fail? Was the reasoning sound but the final answer malformed? The scalar tells you nothing. To extract signal, RL must compare many trajectories—<em>this one scored 0.8, that one scored 0.4, what differed?</em>—requiring sample counts that expensive domains can’t support.</p>
<blockquote class="blockquote">
<p><em>RL and fine-tuning require generating large amounts of rollouts to gather scalar learning signals—sample inefficient by design.</em></p>
</blockquote>
<p>When each rollout costs minutes (or dollars), this approach breaks down.</p>
</section>
<section id="the-insight-llm-pipelines-generate-rich-textual-traces" class="level3">
<h3 class="anchored" data-anchor-id="the-insight-llm-pipelines-generate-rich-textual-traces">The Insight: LLM Pipelines Generate Rich Textual Traces</h3>
<p>Modern AI systems built around LLMs are fundamentally different from traditional ML pipelines. At every step of execution, they produce natural language artifacts that traditional optimization simply discards:</p>
<ul>
<li><strong>Reasoning traces</strong> — Chain-of-Thought and ReAct logs expose the model’s explicit thought process. When a multi-hop QA system fails, you can see <em>where</em> the reasoning went wrong: “The capital of France is Paris. Paris is in Germany…” The failure mode is visible in the text.</li>
<li><strong>Environment feedback</strong> — Compiler errors don’t just say “failed.” They say <code>cannot find symbol 'x', did you mean 'y'?</code> API responses return structured explanations. Profilers report exactly which function consumed 80% of runtime.</li>
<li><strong>Evaluation rubrics</strong> — LLM-as-judge systems don’t just score 3/5. They explain: “Response was accurate but exceeded the 200-word limit. Missing the requested bullet-point format. Tone too formal for the specified Slack context.”</li>
</ul>
<p>Each of these is far more informative than <code>reward = 0.73</code>.</p>
<p>But here’s the thing: these traces aren’t just logs for debugging—they’re potential <em>input to the optimizer</em>. A compiler error that says “did you mean ‘y’?” contains the fix. A rubric that says “too verbose” specifies exactly what to change.</p>
<p>Traditional RL ignores all of this. It reduces the entire trajectory to a scalar, then tries to reconstruct what went wrong by comparing thousands of trajectories.</p>
<p>But what if we could just… read the feedback?</p>
<p>This is the opportunity GEPA exploits. The question becomes: can an LLM reflect on these traces and propose improvements directly?</p>
</section>
<section id="the-opportunity-llms-can-reflect-on-their-own-failures" class="level3">
<h3 class="anchored" data-anchor-id="the-opportunity-llms-can-reflect-on-their-own-failures">The Opportunity: LLMs Can Reflect on Their Own Failures</h3>
<p><strong>LLMs already have prior knowledge about the domains they’re working in, and they’re increasingly capable of self-reflection.</strong></p>
<p>Consider what happens with different types of feedback:</p>
<p><strong>Compiler errors</strong> — When the compiler returns <code>cannot find symbol 'x', did you mean 'y'?</code>, the LLM doesn’t need thousands of examples to learn the fix. It already knows the library’s API. One error message is enough.</p>
<blockquote class="blockquote">
<p><em>“The language model already knows that x is not a valid API name in the library but y is. Next time I should try this.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p><strong>LLM-as-judge feedback</strong> — When a judge says “your summary was accurate but exceeded the 200-word limit and used overly formal tone for Slack,” the model can directly incorporate “be concise, match casual tone” into its next attempt. No statistical signal extraction required.</p>
<p><strong>Reasoning trace failures</strong> — When a multi-hop QA trace shows the model correctly retrieved “Paris is the capital of France” but then hallucinated “Paris is in Germany,” the failure point is <em>visible in the text</em>. You can see exactly where the reasoning derailed.</p>
<p><strong>Privacy-aware rewriting (PUPA task)</strong> — In the paper’s experiments, an LLM must rewrite prompts to remove private information while preserving response quality. The LLM-as-judge explains <em>why</em> a rewrite failed—“leaked the user’s company name” or “removed too much context, degrading response quality”—giving the optimizer actionable signal from each example.</p>
<section id="how-they-compare" class="level4">
<h4 class="anchored" data-anchor-id="how-they-compare">How They Compare</h4>
<table class="caption-top table">
<colgroup>
<col style="width: 40%">
<col style="width: 60%">
</colgroup>
<thead>
<tr class="header">
<th>Approach</th>
<th>How it learns</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>RL</strong></td>
<td>Compare thousands of trajectories statistically: “These 500 scored 0.8, those 500 scored 0.4—what differed?”</td>
</tr>
<tr class="even">
<td><strong>Reflection</strong></td>
<td>Read the feedback directly: “The compiler said use y, so use y.”</td>
</tr>
</tbody>
</table>
<p>RL would need hundreds of rollouts to statistically isolate that <code>x→y</code> is the fix. The LLM gets it from one error message.</p>
</section>
<section id="from-fixes-to-generalizable-rules" class="level4">
<h4 class="anchored" data-anchor-id="from-fixes-to-generalizable-rules">From Fixes to Generalizable Rules</h4>
<p>Modern LLMs don’t just extract point fixes—they can derive <em>generalizable lessons</em>:</p>
<ul>
<li>Beyond “use <code>y</code> instead of <code>x</code>”: “always verify symbol names against the library’s namespace before generating code”</li>
<li>Beyond “response was too long”: “for Slack contexts, limit responses to 150 words and use bullet points”</li>
<li>Beyond “leaked company name”: “scan for proper nouns and replace with generic placeholders”</li>
</ul>
<blockquote class="blockquote">
<p><em>“LLMs can reflect on their own entire trajectories and extract lessons or generalizable rules that can be incorporated into the prompt.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p>These rules get folded directly into the prompt as instructions—accumulating improvements across examples rather than treating each failure in isolation.</p>
<p>LLMs that can genuinely reflect and generalize make this approach viable now.</p>
</section>
</section>
<section id="why-this-works-now" class="level3">
<h3 class="anchored" data-anchor-id="why-this-works-now">Why This Works Now</h3>
<p>This approach wasn’t viable with earlier LLMs. In March 2023, roboticist <a href="https://evjang.com/2023/03/26/self-reflection.html">Eric Jang observed</a> that self-reflection capability “seems to be emergent in GPT-4 but not GPT-3.5 or Claude.” When asked to write a non-rhyming poem, GPT-4 produced rhymes—but when prompted “did the poem meet the assignment?” it apologized and corrected itself. GPT-3.5 and Claude couldn’t recognize their errors.</p>
<p>The <a href="https://arxiv.org/abs/2303.11366">Reflexion paper</a> (NeurIPS 2023) demonstrated the impact quantitatively: by maintaining verbal reflections across trials, LLMs achieved 91% on HumanEval coding benchmark versus GPT-4’s baseline 80%—without any weight updates. Similarly, <a href="https://arxiv.org/abs/2303.17651">Self-Refine</a> showed ~20% average improvement by having the same LLM generate, critique, and refine iteratively.</p>
<p>But there’s a catch. A <a href="https://aclanthology.org/2024.tacl-1.78/">comprehensive 2024 survey</a> found that pure “intrinsic” self-correction—where the LLM reflects with no external signal—rarely helps, and can even degrade performance. What <em>does</em> work is self-correction with <strong>reliable external feedback</strong>: compiler errors, test results, structured rubrics.</p>
<p>GEPA exploits exactly this. The <a href="https://arxiv.org/abs/2305.11738">CRITIC paper</a> (ICLR 2024) highlights that external feedback is “crucial” for successful self-improvement. GEPA doesn’t ask the LLM to magically know it was wrong. It feeds the LLM rich textual feedback—the compiler said this, the profiler showed that, the judge flagged this rubric—and asks it to reflect on <em>that</em>.</p>
<p>The shift: LLMs can now <em>process feedback and generalize lessons</em> effectively—not that they introspect perfectly.</p>
<blockquote class="blockquote">
<p><em>“Earlier LLMs could not actually reflect that well on their trajectories and extract meaningful insights or lessons… But now we are seeing that as LLMs are getting better they can also reflect on their own entire trajectories and extract lessons that can be incorporated into the prompt.”</em> — Lakshya A Agrawal</p>
</blockquote>
</section>
<section id="optimizer-evolution-from-few-shot-to-reflection" class="level3">
<h3 class="anchored" data-anchor-id="optimizer-evolution-from-few-shot-to-reflection">Optimizer Evolution: From Few-Shot to Reflection</h3>
<p>To understand where GEPA fits, it helps to trace the lineage of prompt optimizers. Each generation solved a limitation of its predecessor—and GEPA represents the latest capability unlock.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/gepa-deepdive/static/optimizer evolution.webp" class="img-fluid figure-img"></p>
<figcaption>Optimizer Evolution</figcaption>
</figure>
</div>
<section id="bootstrap-few-shot-dspy-2023" class="level4">
<h4 class="anchored" data-anchor-id="bootstrap-few-shot-dspy-2023">Bootstrap Few-Shot (DSPy, 2023)</h4>
<p>The original insight: you don’t need hand-crafted demonstrations. Given a task and metric, run the pipeline on your training examples, score the outputs, and keep the high-scoring (input, output) pairs as <a href="https://www.promptingguide.ai/techniques/fewshot">few-shot demonstrations</a> for future runs. The system bootstraps its own examples from successful executions.</p>
<p><strong>Example</strong>: Your QA system correctly answers “What’s the capital of France?” → “Paris”. That (question, answer) pair becomes a demonstration shown to the model on future queries.</p>
<p><strong>Limitation</strong>: Demonstrations are static snapshots. Once selected, they don’t adapt when new failure modes emerge. And there’s no instruction optimization — the system prompt stays identical whether you’re handling edge cases or common inputs.</p>
</section>
<section id="opro-google-deepmind-2023" class="level4">
<h4 class="anchored" data-anchor-id="opro-google-deepmind-2023">OPRO (Google DeepMind, 2023)</h4>
<p><a href="https://arxiv.org/abs/2309.03409">OPRO</a> (Optimization by PROmpting, NeurIPS 2023) introduced the idea of using an LLM as the optimizer itself. The key mechanism: show the LLM a history of prompts and their scores, then ask it to propose a better one. Higher-scoring prompts appear more frequently in this history, nudging the LLM toward successful patterns.</p>
<p><strong>Example</strong>: The optimizer sees:</p>
<ul>
<li><code>"Solve the math problem step by step"</code> → score 0.65</li>
<li><code>"Show your work and verify the answer"</code> → score 0.72</li>
<li><code>"Break the problem into cases and check each"</code> → score 0.78</li>
</ul>
<p>It proposes: <code>"Systematically enumerate cases and verify each solution"</code> → score 0.81</p>
<p><strong>Limitation</strong>: Score-only signal. The optimizer sees <em>that</em> <code>prompt_v3</code> scored 0.72 but not <em>why</em>. Did it make algebraic errors? Miss edge cases? The number alone doesn’t say.</p>
</section>
<section id="mipro-2024" class="level4">
<h4 class="anchored" data-anchor-id="mipro-2024">MiPRO (2024)</h4>
<p><a href="https://arxiv.org/abs/2406.11695">MiPRO</a> recognized that instructions and demonstrations interact—the <em>same</em> instruction performs differently with different example sets. The best instruction with bad demos might score worse than a mediocre instruction with perfect demos.</p>
<p><strong>The search space problem</strong>: Say you have 10 candidate instructions and 5 possible demo sets. That’s 50 combinations. Now add instruction variants (“Be concise” vs “Be brief” vs “Answer in one sentence”)—suddenly you have hundreds of candidates. Each full evaluation means running your pipeline on your entire dev set. At $0.10 per run with 100 dev examples, evaluating all 500 combinations costs $5,000. Not feasible.</p>
<p><strong>MiPRO’s solution: a cheap surrogate model</strong>. Instead of running the full pipeline, MiPRO trains a small predictor (think: logistic regression) on the evaluations you <em>have</em> run. The predictor learns patterns like “instructions mentioning ‘step-by-step’ tend to score higher” or “demos with longer reasoning traces correlate with better performance.”</p>
<p>The workflow:</p>
<ol type="1">
<li><strong>Bootstrap</strong>: Run a small random sample of combinations (say, 30 out of 500)</li>
<li><strong>Train surrogate</strong>: Fit the predictor on those 30 (instruction, demos) → score pairs</li>
<li><strong>Predict cheaply</strong>: Score all 500 combinations using the surrogate (milliseconds, not dollars)</li>
<li><strong>Evaluate selectively</strong>: Only run full evaluation on the top-predicted candidates</li>
<li><strong>Repeat</strong>: Add new results to training data, retrain surrogate, sample again</li>
</ol>
<p><strong>Example</strong>: After 30 random evaluations, the surrogate learns:</p>
<ul>
<li>Instructions with “step-by-step” → +0.08 average</li>
<li>Demo set B (which has chain-of-thought examples) → +0.05 average</li>
<li>Combining both → predicted 0.79</li>
</ul>
<p>MiPRO focuses budget on high-predicted combinations rather than exhaustive search.</p>
<p><strong>Limitation</strong>: The surrogate learns <em>correlations</em>, not <em>causation</em>. It knows “step-by-step instructions score higher” but not <em>why</em>—maybe they help on math problems but hurt on simple lookups. And MiPRO optimizes for aggregate score: a prompt that’s 0.75 on everything beats one that’s 0.95 on hard cases but 0.60 overall—even though that hard-case specialist might contain crucial insights.</p>
</section>
<section id="simba-dspy-2024" class="level4">
<h4 class="anchored" data-anchor-id="simba-dspy-2024">SIMBA (DSPy, 2024)</h4>
<p><a href="https://dspy.ai/api/optimizers/SIMBA/">SIMBA</a> (Stochastic Introspective Mini-Batch Ascent) introduced self-reflection into prompt optimization. Rather than treating prompts as black boxes with hidden payout rates, SIMBA has the LLM analyze its own performance and propose improvements.</p>
<p><strong>How it works</strong>:</p>
<ol type="1">
<li><strong>Sample mini-batches</strong>: Instead of evaluating on the full dataset, SIMBA samples small batches of examples</li>
<li><strong>Identify hard cases</strong>: Track which examples show high output variability or consistent failures</li>
<li><strong>Generate reflective rules</strong>: Ask the LLM to analyze why those cases failed and propose improvement rules</li>
<li><strong>Update prompts</strong>: Incorporate the rules as new instructions, or add successful examples as demonstrations</li>
<li><strong>Repeat</strong>: Iterate with new mini-batches, accumulating insights</li>
</ol>
<p><strong>Example</strong>: After several mini-batches, SIMBA notices examples 7 and 12 consistently fail. It prompts the LLM: “These examples failed. What pattern do you see?” The LLM reflects: “Both involve multi-step calculations where I lost track of units.” SIMBA adds: “Always track units through each calculation step.”</p>
<p><strong>The key innovation</strong>: SIMBA bridges the gap between score-only optimization (OPRO, MiPRO) and full trajectory reflection (GEPA). It uses introspection on failure patterns rather than just comparing aggregate scores.</p>
<p><strong>Limitation</strong>: SIMBA’s reflection is still relatively shallow—it identifies patterns across examples but doesn’t deeply analyze individual execution traces. GEPA extends this by feeding the optimizer rich textual feedback (compiler errors, rubrics, reasoning traces) from each trajectory.</p>
</section>
<section id="gepa-2025-the-reflection-shift" class="level4">
<h4 class="anchored" data-anchor-id="gepa-2025-the-reflection-shift">GEPA (2025): The Reflection Shift</h4>
<p>GEPA breaks from this trajectory in two ways:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 36%">
<col style="width: 34%">
<col style="width: 28%">
</colgroup>
<thead>
<tr class="header">
<th>What changed</th>
<th>Before GEPA</th>
<th>With GEPA</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Learning signal</strong></td>
<td><code>score = 0.6</code></td>
<td>“Exceeded word limit. Missing keyword. Compiler error: use y not x.”</td>
</tr>
<tr class="even">
<td><strong>Selection strategy</strong></td>
<td>Best aggregate score</td>
<td>Pareto frontier of diverse specialists</td>
</tr>
</tbody>
</table>
<p><strong>1. From scalar scores to textual feedback</strong> — Instead of just knowing <em>that</em> a prompt scored 0.6, GEPA sees <em>why</em>: the compiler error, the rubric failures, the reasoning trace where hallucination occurred. The optimizer reads the feedback directly.</p>
<p><strong>Example</strong>: OPRO sees <code>score = 0.6</code>. GEPA sees: &gt; “Failed on example 7: response was 340 words (limit: 200). Failed on example 12: missing required keyword ‘disclaimer’. Passed examples 1-6, 8-11.”</p>
<p>The LLM reflects: “I should add an instruction about word limits and required keywords.”</p>
<p><strong>2. From greedy to Pareto selection</strong> — Instead of always promoting the highest-scoring candidate, GEPA maintains a <em><a href="https://en.wikipedia.org/wiki/Pareto_efficiency">Pareto frontier</a></em>: candidates that each excel at <em>something</em> no other candidate beats.</p>
<p><strong>Example</strong>: Three candidates evaluated on 10 examples:</p>
<ul>
<li><code>prompt_A</code>: 8/10 overall, but fails hard cases #7 and #9</li>
<li><code>prompt_B</code>: 6/10 overall, but nails hard cases #7 and #9<br>
</li>
<li><code>prompt_C</code>: 7/10 overall, no unique strengths</li>
</ul>
<p>Greedy selection keeps only <code>prompt_A</code>. Pareto selection keeps both <code>prompt_A</code> <em>and</em> <code>prompt_B</code>—because B’s insights about hard cases might combine with A’s general strength. <code>prompt_C</code> gets dropped (dominated by A on everything).</p>
<p>The difference: OPRO knows the score dropped from 0.8 to 0.6. GEPA reads the compiler error that explains why—and proposes the fix directly.</p>
</section>
</section>
<section id="how-gepa-works-building-it-from-scratch" class="level2">
<h2 class="anchored" data-anchor-id="how-gepa-works-building-it-from-scratch">How GEPA Works: Building It From Scratch</h2>
<p>GEPA combines two key innovations: <strong>reflective prompt mutation</strong> (learning from textual feedback) and <strong>Pareto selection</strong> (preserving diverse specialists). Each helps on its own; combined, they reinforce each other.</p>
<p>We’ll build them from scratch in this section:</p>
<ol type="1">
<li><p><strong>Reflective mutation</strong> — How GEPA extracts generalizable lessons from rollout traces and feedback, proposing improved prompts directly. This is where the sample efficiency comes from.</p></li>
<li><p><strong>Pareto selection</strong> — Why always improving your “best” prompt gets stuck, and how tracking per-instance performance preserves insights that would otherwise be lost.</p></li>
<li><p><strong>Merge</strong> — How GEPA combines insights from divergent lineages (covered after the complete algorithm).</p></li>
</ol>
<p>By the end, you’ll see how these mechanisms combine into GEPA’s full evolutionary loop.</p>
<hr>
<section id="quick-start-using-gepa-in-30-seconds" class="level3">
<h3 class="anchored" data-anchor-id="quick-start-using-gepa-in-30-seconds">Quick Start: Using GEPA in 30 Seconds</h3>
<blockquote class="blockquote">
<p>📖 <strong>Full tutorial</strong>: <a href="https://dspy.ai/tutorials/gepa_aime/">GEPA for AIME (Math)</a> — optimizing GPT-4.1 Mini from 46.6% → 56.6% on AIME 2025.</p>
</blockquote>
<p>Before diving deep, here’s what using GEPA looks like in practice:</p>
<p><strong>Step 1: Configure your language model</strong></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> dspy</span>
<span id="cb1-2"></span>
<span id="cb1-3">lm <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dspy.LM(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"openai/gpt-4.1-mini"</span>, temperature<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, max_tokens<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">32000</span>)</span>
<span id="cb1-4">dspy.configure(lm<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>lm)</span></code></pre></div></div>
<p><strong>Step 2: Define your program</strong></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1">program <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dspy.ChainOfThought(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"problem -&gt; answer"</span>)</span></code></pre></div></div>
<p><strong>Step 3: Define a metric that returns feedback (not just a score)</strong></p>
<p>This is the key difference from other optimizers—your metric explains <em>why</em> something failed:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> metric_with_feedback(example, prediction, trace<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span>kwargs):</span>
<span id="cb3-2">    correct_answer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> example.answer</span>
<span id="cb3-3">    pred_answer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> prediction.answer</span>
<span id="cb3-4">    </span>
<span id="cb3-5">    score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(correct_answer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> pred_answer)</span>
<span id="cb3-6">    </span>
<span id="cb3-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>:</span>
<span id="cb3-8">        feedback <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Correct! The answer is '</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>correct_answer<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">'."</span></span>
<span id="cb3-9">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span>:</span>
<span id="cb3-10">        feedback <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Incorrect. Expected '</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>correct_answer<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">', got '</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>pred_answer<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">'."</span></span>
<span id="cb3-11">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add any additional context that could help improvement:</span></span>
<span id="cb3-12">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">hasattr</span>(example, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'solution'</span>):</span>
<span id="cb3-13">            feedback <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f" Solution: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>example<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>solution<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-14">    </span>
<span id="cb3-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> dspy.Prediction(score<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>score, feedback<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>feedback)</span></code></pre></div></div>
<p><strong>Step 4: Optimize with GEPA</strong></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> dspy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> GEPA</span>
<span id="cb4-2"></span>
<span id="cb4-3">optimizer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> GEPA(</span>
<span id="cb4-4">    metric<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>metric_with_feedback,</span>
<span id="cb4-5">    auto<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"light"</span>,           <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Budget preset: "light", "medium", or "heavy"</span></span>
<span id="cb4-6">    num_threads<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">32</span>,         <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Parallel evaluation threads</span></span>
<span id="cb4-7">)</span>
<span id="cb4-8"></span>
<span id="cb4-9">optimized_program <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> optimizer.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">compile</span>(</span>
<span id="cb4-10">    program,</span>
<span id="cb4-11">    trainset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>train_set,</span>
<span id="cb4-12">    valset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>val_set,</span>
<span id="cb4-13">)</span></code></pre></div></div>
<p>GEPA handles the evolutionary loop, Pareto selection, and reflective mutation internally. If you just want to use it, the code above is sufficient—see the <a href="https://dspy.ai/api/optimizers/GEPA/overview/">DSPy GEPA API Reference</a> for full parameter details.</p>
<p>But what’s actually happening? Your metric returns <em>why</em> something failed (not just a score), an LLM reads that feedback and proposes improved instructions, Pareto selection preserves diverse specialists rather than just the highest-scoring prompt, and merge operations combine insights from divergent lineages.</p>
<p>The rest of this section builds the core mechanism from scratch.</p>
</section>
<section id="gepa-reflective-prompt-evolution-flow-diagram-from-paper" class="level3">
<h3 class="anchored" data-anchor-id="gepa-reflective-prompt-evolution-flow-diagram-from-paper">GEPA: REFLECTIVE PROMPT EVOLUTION (Flow diagram from paper)</h3>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/gepa-deepdive/static/GEPA_flowchart.png" class="img-fluid figure-img"></p>
<figcaption>GEPA Flowchart</figcaption>
</figure>
</div>
<p>The diagram above shows GEPA’s loop. Each prompt is evaluated on every training task, producing a per-instance score matrix. Pareto filtering preserves prompts that excel at something no other prompt beats. New candidates come from <strong>reflective mutation</strong> (analyzing textual feedback) or <strong>merge</strong> (combining two specialists’ insights). Only candidates passing a minibatch screen get full evaluation.</p>
<p>The next section implements reflective mutation: the “Reflect and Propose New Prompt” step in the diagram.</p>
</section>
<section id="hands-on-building-reflective-mutation-from-scratch" class="level3">
<h3 class="anchored" data-anchor-id="hands-on-building-reflective-mutation-from-scratch">Hands-On: Building Reflective Mutation from Scratch</h3>
<p>To see how this works by implementing GEPA’s core mechanism on a real task.</p>
<p><strong>The Problem: AIME Math Competition</strong></p>
<p>We’ll optimize prompts for solving <a href="https://en.wikipedia.org/wiki/American_Invitational_Mathematics_Examination">AIME</a> (American Invitational Mathematics Examination) problems — challenging competition math that tests algebra, number theory, geometry, and combinatorics. These problems are hard: even frontier LLMs struggle without careful prompting.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> datasets <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> load_dataset</span>
<span id="cb5-2">dset <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> load_dataset(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"AI-MO/aimo-validation-aime"</span>)[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'train'</span>]</span>
<span id="cb5-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 90 problems with solutions and integer answers</span></span></code></pre></div></div>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 37%">
<col style="width: 29%">
</colgroup>
<thead>
<tr class="header">
<th>problem</th>
<th>solution</th>
<th>answer</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Quadratic polynomials <img src="https://latex.codecogs.com/png.latex?P(x)"> and <img src="https://latex.codecogs.com/png.latex?Q(x)"> have l…</td>
<td>Let <img src="https://latex.codecogs.com/png.latex?R(x)=P(x)+Q(x)."> Since the <img src="https://latex.codecogs.com/png.latex?x%5E2">-terms of…</td>
<td>116</td>
</tr>
<tr class="even">
<td>Three spheres with radii <img src="https://latex.codecogs.com/png.latex?11">, <img src="https://latex.codecogs.com/png.latex?13">, and <img src="https://latex.codecogs.com/png.latex?19"> …</td>
<td>This solution refers to the Diagram section…</td>
<td>756</td>
</tr>
</tbody>
</table>
<p><strong>Why AIME for testing prompt optimization?</strong></p>
<ol type="1">
<li><strong>Clear ground truth</strong> — Every answer is an integer (0-999), so evaluation is unambiguous</li>
<li><strong>Rich failure modes</strong> — Wrong answers come from algebraic errors, missed cases, misread constraints</li>
<li><strong>Domain knowledge helps</strong> — Prompts that encode strategies (“subtract equations pairwise”, “enumerate all cases”) measurably improve performance</li>
<li><strong>Small dataset</strong> — Only 90 problems, so sample efficiency matters</li>
</ol>
<p><strong>The Setup</strong></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Split: 10 train, 10 validation (simulating scarce labeled data)</span></span>
<span id="cb6-2">tdf <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> df.sample(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">45</span>).iloc[:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Training mini-batches drawn from here</span></span>
<span id="cb6-3">vdf <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> df.drop(tdf.index).iloc[:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Held-out validation</span></span>
<span id="cb6-4"></span>
<span id="cb6-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Base model: Gemini 2.5 Flash via LiteLLM</span></span>
<span id="cb6-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Metric: Exact match (predicted integer == ground truth)</span></span>
<span id="cb6-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> metric(ground_truth, prediction):</span>
<span id="cb6-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(ground_truth) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> prediction[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'answer'</span>]</span></code></pre></div></div>
<p><strong>Seed Prompt</strong></p>
<p>We start with a minimal instruction:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1">seed_prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""You are given a problem and you have to give the answer </span></span>
<span id="cb7-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">along with reasoning. Do not return anything apart from json. </span></span>
<span id="cb7-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">It should be parsable by json.loads()"""</span></span></code></pre></div></div>
<p>Baseline validation accuracy: <strong>10%</strong> (1/10 correct)</p>
<p>Can reflective mutation improve this? Let’s find out.</p>
<hr>
</section>
<section id="step-1-the-feedback-function" class="level3">
<h3 class="anchored" data-anchor-id="step-1-the-feedback-function">Step 1: The Feedback Function</h3>
<p>First, we need a function that tells the reflection LLM what went wrong. We’ll start with <strong>minimal feedback</strong>—just the correct answer:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> feedback(ground_truth, prediction):</span>
<span id="cb8-2">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(ground_truth) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> prediction[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'answer'</span>]:</span>
<span id="cb8-3">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'You got it wrong! The solution is </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>ground_truth<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span></span>
<span id="cb8-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'You got it right!'</span></span></code></pre></div></div>
<p>This is deliberately simple. Later we’ll discuss how richer feedback (like expert solutions) can improve results.</p>
<hr>
</section>
<section id="step-2-the-reflection-prompt" class="level3">
<h3 class="anchored" data-anchor-id="step-2-the-reflection-prompt">Step 2: The Reflection Prompt</h3>
<p>Following GEPA’s structure, we build a prompt that shows the LLM its failures:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb9-1">REFLECTION_TEMPLATE <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""I provided an assistant with the following instructions:</span></span>
<span id="cb9-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">&lt;curr_instructions&gt;</span></span>
<span id="cb9-3"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{current_prompt}</span></span>
<span id="cb9-4"></span>
<span id="cb9-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">The following are examples with assistant's responses and feedback:</span></span>
<span id="cb9-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">&lt;inputs_outputs_feedback&gt;</span></span>
<span id="cb9-7"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{examples}</span></span>
<span id="cb9-8"></span>
<span id="cb9-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Your task: write a new instruction for the assistant.</span></span>
<span id="cb9-10"></span>
<span id="cb9-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">- Read inputs carefully and identify the input format and task description</span></span>
<span id="cb9-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">- Read all responses and feedback. Identify niche/domain-specific factual information</span></span>
<span id="cb9-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">- If the assistant used a generalizable strategy, include that in the instruction</span></span>
<span id="cb9-14"></span>
<span id="cb9-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Provide the new instructions.</span></span>
<span id="cb9-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb9-17"></span>
<span id="cb9-18"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> mk_reflection_prompt(df, curr_prompt):</span>
<span id="cb9-19">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Build reflection prompt from minibatch results."""</span></span>
<span id="cb9-20">    examples <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb9-21">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i, row <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> df.reset_index().iterrows():</span>
<span id="cb9-22">        example <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"""# Example </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-23"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">## problem</span></span>
<span id="cb9-24"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>row[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'problem'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-25"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">## prediction</span></span>
<span id="cb9-26"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>row[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'pred'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-27"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">## feedback</span></span>
<span id="cb9-28"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>feedback(row.answer, row.pred)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-29"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb9-30">        examples.append(example)</span>
<span id="cb9-31">    </span>
<span id="cb9-32">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> REFLECTION_TEMPLATE.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">format</span>(</span>
<span id="cb9-33">        current_prompt<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>curr_prompt,</span>
<span id="cb9-34">        examples<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>.join(examples)</span>
<span id="cb9-35">    )</span></code></pre></div></div>
<p><strong>Example filled-in reflection prompt:</strong></p>
<pre><code>I provided an assistant with the following instructions:
&lt;curr_instructions&gt;
You are given a problem and you have to give the answer along with reasoning. 
Do not return anything apart from json. It should be parsable by json.loads()

The following are examples with assistant's responses and feedback:
&lt;inputs_outputs_feedback&gt;
# Example 1
## problem
Quadratic polynomials P(x) and Q(x) have leading coefficient 1. The sum of the roots of P(x) is 7...
## prediction
{"answer": 42, "reasoning": "I solved the system and got x=7, y=6"}
## feedback
You got it wrong! The solution is 116

# Example 2
## problem
Three spheres with radii 11, 13, and 19 are mutually externally tangent...
## prediction
{"answer": 756, "reasoning": "Using the tangent sphere formula..."}
## feedback
You got it right!

# Example 3
## problem
Find the remainder when 2^2024 is divided by 1000...
## prediction
{"answer": 16, "reasoning": "I computed powers of 2 mod 1000..."}
## feedback
You got it wrong! The solution is 896

Your task: write a new instruction for the assistant.

- Read inputs carefully and identify the input format and task description
- Read all responses and feedback. Identify niche/domain-specific factual information
- If the assistant used a generalizable strategy, include that in the instruction

Provide the new instructions.</code></pre>
<hr>
</section>
<section id="step-3-the-complete-optimization-loop" class="level3">
<h3 class="anchored" data-anchor-id="step-3-the-complete-optimization-loop">Step 3: The Complete Optimization Loop</h3>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb11-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> mk_reflection_prompt(mb, curr_prompt):</span>
<span id="cb11-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Build reflection prompt from minibatch results."""</span></span>
<span id="cb11-3">    examples <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb11-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i, row <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> df.reset_index().iterrows():</span>
<span id="cb11-5">        example <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"""# Example </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb11-6"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">## problem</span></span>
<span id="cb11-7"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>row[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'problem'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb11-8"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">## prediction</span></span>
<span id="cb11-9"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>row[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'pred'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb11-10"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">## feedback</span></span>
<span id="cb11-11"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>feedback(row.answer, row.pred)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb11-12"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb11-13">        examples.append(example)</span>
<span id="cb11-14">    </span>
<span id="cb11-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> REFLECTION_TEMPLATE.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">format</span>(</span>
<span id="cb11-16">        current_prompt<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>curr_prompt,</span>
<span id="cb11-17">        examples<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>.join(examples)</span>
<span id="cb11-18">    )</span>
<span id="cb11-19"></span>
<span id="cb11-20"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> reflect(mb, curr_prompt):</span>
<span id="cb11-21">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Ask LLM to reflect on failures and propose improved instruction."""</span></span>
<span id="cb11-22">    refl_prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> mk_reflection_prompt(mb, curr_prompt)</span>
<span id="cb11-23">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> _call(refl_prompt, <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">format</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>ReflectionModel)[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'new_instruction'</span>]</span>
<span id="cb11-24"></span>
<span id="cb11-25"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> optimize_prompt(seed_prompt, traindf, valdf, n_iters<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, mb_size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>):</span>
<span id="cb11-26">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Greedy reflective prompt optimization."""</span></span>
<span id="cb11-27">    prompts, train_scores, val_scores <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [seed_prompt], [], []</span>
<span id="cb11-28">    mb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> traindf.sample(mb_size)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Fixed minibatch for this run</span></span>
<span id="cb11-29">    </span>
<span id="cb11-30">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'Baseline validation: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>eval_val(seed_prompt, valdf)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2%}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span>)</span>
<span id="cb11-31">    </span>
<span id="cb11-32">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(n_iters):</span>
<span id="cb11-33">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Evaluate current prompt on minibatch</span></span>
<span id="cb11-34">        mb_eval, mb_score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> eval_mb(prompts[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], mb)</span>
<span id="cb11-35">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"📊 Minibatch: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>mb_score<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2%}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb11-36">        </span>
<span id="cb11-37">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Reflect and propose new instruction</span></span>
<span id="cb11-38">        new_instr <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> reflect(mb_eval, prompts[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])</span>
<span id="cb11-39">        new_prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> new_instr  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># The new instruction becomes the new prompt</span></span>
<span id="cb11-40">        </span>
<span id="cb11-41">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Evaluate on validation set</span></span>
<span id="cb11-42">        val_score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> eval_val(new_prompt, valdf)</span>
<span id="cb11-43">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"📊 Validation: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>val_score<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2%}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb11-44">        </span>
<span id="cb11-45">        prompts.append(new_prompt)</span>
<span id="cb11-46">        val_scores.append(val_score)</span>
<span id="cb11-47">    </span>
<span id="cb11-48">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">dict</span>(prompts<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>prompts, val_scores<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>val_scores)</span></code></pre></div></div>
<hr>
</section>
<section id="what-actually-happened" class="level3">
<h3 class="anchored" data-anchor-id="what-actually-happened">What Actually Happened</h3>
<p>Running this on AIME problems with Gemini 2.5 Flash:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 17%">
<col style="width: 17%">
<col style="width: 19%">
<col style="width: 45%">
</colgroup>
<thead>
<tr class="header">
<th>Iteration</th>
<th>Minibatch</th>
<th>Validation</th>
<th>What the reflection learned</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Baseline</td>
<td>—</td>
<td>10%</td>
<td>—</td>
</tr>
<tr class="even">
<td>1</td>
<td>0%</td>
<td>10%</td>
<td>JSON formatting details, output structure rules</td>
</tr>
<tr class="odd">
<td>2</td>
<td>0%</td>
<td><strong>30%</strong></td>
<td>Systems of equations strategy, remainder/modular arithmetic tips</td>
</tr>
<tr class="even">
<td>3</td>
<td>67%</td>
<td><strong>10%</strong></td>
<td>Over-specialized on number theory, solved #9 but lost generality</td>
</tr>
</tbody>
</table>
<p><strong>The good</strong>: Iteration 2 extracted genuinely useful domain knowledge. Despite 0% minibatch accuracy, the reflection LLM identified patterns from the problems themselves and added this to the prompt:</p>
<blockquote class="blockquote">
<p><em>“When dealing with systems of equations like <img src="https://latex.codecogs.com/png.latex?xy%20+%20Az%20=%20C">, <img src="https://latex.codecogs.com/png.latex?yz%20+%20Ax%20=%20C">, <img src="https://latex.codecogs.com/png.latex?zx%20+%20Ay%20=%20C">, consider subtracting equations pairwise to find relationships between variables, such as <img src="https://latex.codecogs.com/png.latex?(x-z)(y-A)=0">, which implies <img src="https://latex.codecogs.com/png.latex?x=z"> or <img src="https://latex.codecogs.com/png.latex?y=A">. Systematically explore all such cases.”</em></p>
</blockquote>
<p>This is directly from the actual output—the reflection LLM read the failed attempt on a systems-of-equations problem and generalized a useful heuristic.</p>
<p><strong>The bad</strong>: Iteration 3 achieved 67% on its minibatch but dropped to <strong>10% validation</strong>. Why? The minibatch happened to contain number theory problems, so the reflection added specialized number-theory guidance:</p>
<blockquote class="blockquote">
<p><em>“When the problem involves number theory and remainders (e.g., <img src="https://latex.codecogs.com/png.latex?n%20%5Cpmod%20x">, <img src="https://latex.codecogs.com/png.latex?n%20%5Cpmod%20y">), pay close attention to inconsistencies or contradictions that might arise from given conditions, especially when distinct remainders are required, as these can lead to an answer of 0.”</em></p>
</blockquote>
<p>This over-specialized advice (“can lead to an answer of 0”) actively hurt performance on non-number-theory problems, dropping validation from 30% back to <strong>10%</strong> (1/10)—though notably, it did solve problem #9, which earlier prompts couldn’t.</p>
<hr>
</section>
<section id="the-greedy-selection-problem" class="level3">
<h3 class="anchored" data-anchor-id="the-greedy-selection-problem">The Greedy Selection Problem</h3>
<p>This demonstrates exactly why GEPA uses <strong>Pareto selection</strong> instead of always taking the “best” prompt:</p>
<ol type="1">
<li><strong>Iteration 2’s prompt</strong> was a specialist—it learned something valuable about systems of equations</li>
<li><strong>Iteration 3</strong> tried to improve on iteration 2, but the minibatch had different problems</li>
<li>The reflection <strong>overwrote</strong> the systems-of-equations insight while adding number-theory tips that were <em>too specific</em></li>
<li>Result: <strong>catastrophic forgetting</strong></li>
</ol>
<p>With greedy selection, we would have discarded iteration 2’s valuable insight. Pareto selection would keep it—because it was <em>best on at least one validation instance</em>.</p>
<hr>
</section>
<section id="the-missing-ingredient-rich-feedback" class="level3">
<h3 class="anchored" data-anchor-id="the-missing-ingredient-rich-feedback">The Missing Ingredient: Rich Feedback</h3>
<p>Our minimal feedback (<code>"You got it wrong! The solution is 349"</code>) only tells the model <em>that</em> it failed, not <em>why</em> or <em>how to fix it</em>.</p>
<p>The AIME dataset includes expert solutions. A richer feedback function could use them:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb12-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> feedback_rich(row):</span>
<span id="cb12-2">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(row.answer) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> row.pred[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'answer'</span>]:</span>
<span id="cb12-3">        sol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> row.solution[:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">500</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"..."</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(row.solution) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">500</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> row.solution</span>
<span id="cb12-4">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"""Wrong! Expected </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>row<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>answer<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">, got </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>row<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>pred[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'answer'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">.</span></span>
<span id="cb12-5"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">        </span></span>
<span id="cb12-6"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">Model's reasoning: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>row<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>pred[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'short_reasoning'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb12-7"></span>
<span id="cb12-8"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">Expert solution approach:</span></span>
<span id="cb12-9"><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>sol<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb12-10">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Correct!"</span></span></code></pre></div></div>
<p><strong>Example output</strong> for a wrong answer:</p>
<pre><code>Wrong! Expected 116, got 42.

Model's reasoning: I set up the system of equations and solved for x=7, y=6, giving 7*6=42.

Expert solution approach:
Let R(x)=P(x)+Q(x). Since the x²-terms of P and Q have leading coefficient 1, 
R(x) is quadratic with leading coefficient 2. Given the roots condition, we can 
write R(x) = 2(x-r₁)(x-r₂). Expanding and comparing coefficients...</code></pre>
<p>With rich feedback, the reflection LLM can extract <em>specific strategies</em> from the expert solution rather than having to guess what went wrong. This is what makes GEPA sample-efficient: <strong>the feedback contains the fix</strong>.</p>
<p>Compare this to RL, which would only see <code>reward = 0</code> and have to statistically infer what went wrong across thousands of trajectories.</p>
<hr>
</section>
<section id="what-we-learned" class="level3">
<h3 class="anchored" data-anchor-id="what-we-learned">What We Learned</h3>
<ol type="1">
<li><strong>Reflective mutation works</strong> — Even with minimal feedback, the LLM extracted useful domain knowledge</li>
<li><strong>Greedy selection fails</strong> — Iteration 3’s collapse shows why we need to preserve specialist insights</li>
<li><strong>Feedback quality matters</strong> — Rich feedback (expert solutions, compiler errors, rubric explanations) gives the reflection LLM more to work with</li>
<li><strong>Sample efficiency is real</strong> — We saw meaningful optimization with just 3 iterations on 3 examples each</li>
</ol>
<p>This is the core limitation of greedy optimization: <strong>catastrophic forgetting</strong>. The solution? Pareto selection—which we’ll build from scratch next.</p>
</section>
<section id="hands-on-building-the-pareto-frontier" class="level3">
<h3 class="anchored" data-anchor-id="hands-on-building-the-pareto-frontier">Hands-On: Building the Pareto Frontier</h3>
<p>In the reflective mutation section, we saw greedy selection fail—iteration 3’s over-specialized prompt dropped validation from 30% to 10%, losing iteration 2’s valuable systems-of-equations insights. The fundamental problem: always improving your “best” prompt discards specialist knowledge.</p>
<p>GEPA’s solution: <strong>Pareto selection</strong>. Instead of keeping one best prompt, maintain a <em>frontier</em> of prompts where each excels at something no other prompt beats.</p>
<hr>
<section id="what-is-pareto-dominance" class="level4">
<h4 class="anchored" data-anchor-id="what-is-pareto-dominance">What is Pareto Dominance?</h4>
<p>A prompt <strong>dominates</strong> another if it’s at least as good everywhere, and strictly better somewhere:</p>
<ul>
<li><strong>≥</strong> on <em>every</em> validation instance, AND<br>
</li>
<li><strong>&gt;</strong> on <em>at least one</em> instance</li>
</ul>
<p>If prompt A dominates prompt B, we can safely discard B—A is strictly better in every way that matters. But if neither dominates the other (each wins on different instances), both belong on the frontier.</p>
<p><strong>Example</strong>: Consider four prompts evaluated on 10 validation instances. We’ll use labels P0-P3, which map to our earlier experiment: P0 = Seed, P1 = Iteration 1, P2 = Iteration 2, P3 = Iteration 3:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Prompt</th>
<th>Instances Solved</th>
<th>Aggregate</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>P0 (seed)</td>
<td>#0 only</td>
<td>10%</td>
<td>Dominated by P2</td>
</tr>
<tr class="even">
<td>P1 (iter 1)</td>
<td>#0 only</td>
<td>10%</td>
<td>Dominated by P2</td>
</tr>
<tr class="odd">
<td>P2 (iter 2)</td>
<td>#0, #1, #2</td>
<td>30%</td>
<td><strong>Frontier</strong> ✓</td>
</tr>
<tr class="even">
<td>P3 (iter 3)</td>
<td>#9 only</td>
<td>10%</td>
<td><strong>Frontier</strong> ✓</td>
</tr>
</tbody>
</table>
<p>P2 dominates both P0 and P1—it solves everything they solve, plus more. But P3 survives despite its low aggregate score! It solved instance #9, which <em>nothing else could</em>.</p>
<p>The Pareto frontier is <strong>{P2, P3}</strong>. Both contain unique value.</p>
<hr>
</section>
<section id="implementation-dominance-checking" class="level4">
<h4 class="anchored" data-anchor-id="implementation-dominance-checking">Implementation: Dominance Checking</h4>
<p>We represent per-instance scores as boolean arrays (1 = solved, 0 = failed):</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb14-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb14-2"></span>
<span id="cb14-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> dominates(candidate_scores, other_scores):</span>
<span id="cb14-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Does candidate dominate other? (&gt;= everywhere, &gt; somewhere)"""</span></span>
<span id="cb14-5">    candidate <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array(candidate_scores)</span>
<span id="cb14-6">    other <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array(other_scores)</span>
<span id="cb14-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (candidate <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> other).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">all</span>() <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">and</span> (candidate <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> other).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">any</span>()</span>
<span id="cb14-8"></span>
<span id="cb14-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> is_dominated_by_any(new_scores, frontier_scores):</span>
<span id="cb14-10">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Is new_scores dominated by ANY prompt in the frontier?"""</span></span>
<span id="cb14-11">    new <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array(new_scores)</span>
<span id="cb14-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> existing <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> frontier_scores:</span>
<span id="cb14-13">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> dominates(np.array(existing), new):</span>
<span id="cb14-14">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb14-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span></span>
<span id="cb14-16"></span>
<span id="cb14-17"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> get_dominated_indices(new_scores, frontier_scores):</span>
<span id="cb14-18">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Which frontier prompts does new_scores dominate?"""</span></span>
<span id="cb14-19">    new <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array(new_scores)</span>
<span id="cb14-20">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> [i <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i, existing <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">enumerate</span>(frontier_scores) </span>
<span id="cb14-21">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> dominates(new, np.array(existing))]</span></code></pre></div></div>
<hr>
</section>
<section id="tracing-through-why-p3-survives" class="level4">
<h4 class="anchored" data-anchor-id="tracing-through-why-p3-survives">Tracing Through: Why P3 Survives</h4>
<p>Let’s verify the dominance relationships from our example:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb15-1">P0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Solves: #0</span></span>
<span id="cb15-2">P1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Solves: #0</span></span>
<span id="cb15-3">P2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Solves: #0, #1, #2</span></span>
<span id="cb15-4">P3 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Solves: #9</span></span>
<span id="cb15-5"></span>
<span id="cb15-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Does P2 dominate P0?</span></span>
<span id="cb15-7">dominates(P2, P0)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># True: P2 &gt;= P0 everywhere, P2 &gt; P0 on #1, #2</span></span>
<span id="cb15-8"></span>
<span id="cb15-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Does P2 dominate P1?</span></span>
<span id="cb15-10">dominates(P2, P1)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># True: P2 &gt;= P1 everywhere, P2 &gt; P1 on #1, #2</span></span>
<span id="cb15-11"></span>
<span id="cb15-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Does P2 dominate P3?</span></span>
<span id="cb15-13">dominates(P2, P3)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># False! P2 loses on #9 (0 &lt; 1)</span></span>
<span id="cb15-14"></span>
<span id="cb15-15"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Does P3 dominate P2?</span></span>
<span id="cb15-16">dominates(P3, P2)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># False! P3 loses on #0, #1, #2</span></span></code></pre></div></div>
<p>Neither P2 nor P3 dominates the other—they’re <strong>Pareto incomparable</strong>. Each solves problems the other can’t. Both stay on the frontier.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/gepa-deepdive/static/pareto_visual.webp" class="img-fluid figure-img"></p>
<figcaption>Pareto Frontier Explained</figcaption>
</figure>
</div>
<hr>
</section>
<section id="the-complete-frontier-manager" class="level4">
<h4 class="anchored" data-anchor-id="the-complete-frontier-manager">The Complete Frontier Manager</h4>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb16-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> ParetoFrontier:</span>
<span id="cb16-2">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb16-3">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prompts <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb16-4">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.scores <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># scores[i][j] = prompt i's score on instance j</span></span>
<span id="cb16-5">    </span>
<span id="cb16-6">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> add(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, prompt, instance_scores):</span>
<span id="cb16-7">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Try to add a prompt. Returns True if it joins the frontier."""</span></span>
<span id="cb16-8">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Reject if dominated by existing frontier member</span></span>
<span id="cb16-9">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> is_dominated_by_any(instance_scores, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.scores):</span>
<span id="cb16-10">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span></span>
<span id="cb16-11">        </span>
<span id="cb16-12">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Remove any frontier members this prompt dominates</span></span>
<span id="cb16-13">        dominated <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> get_dominated_indices(instance_scores, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.scores)</span>
<span id="cb16-14">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sorted</span>(dominated, reverse<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>):  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Remove from end first</span></span>
<span id="cb16-15">            <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">del</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prompts[i]</span>
<span id="cb16-16">            <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">del</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.scores[i]</span>
<span id="cb16-17">        </span>
<span id="cb16-18">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add to frontier</span></span>
<span id="cb16-19">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prompts.append(prompt)</span>
<span id="cb16-20">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.scores.append(instance_scores)</span>
<span id="cb16-21">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb16-22">    </span>
<span id="cb16-23">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> sample(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb16-24">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Sample a prompt, weighted by unique wins."""</span></span>
<span id="cb16-25">        weights <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb16-26">        scores_arr <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.scores)</span>
<span id="cb16-27">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prompts)):</span>
<span id="cb16-28">            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># How many instances is this prompt *uniquely* best on?</span></span>
<span id="cb16-29">            others_best <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.delete(scores_arr, i, axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">max</span>(axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prompts) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> np.zeros_like(scores_arr[i])</span>
<span id="cb16-30">            unique_wins <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (scores_arr[i] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> others_best).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>()</span>
<span id="cb16-31">            weights.append(unique_wins <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># +1 smoothing</span></span>
<span id="cb16-32">        </span>
<span id="cb16-33">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.random.choice(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prompts, p<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>np.array(weights)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(weights))</span>
<span id="cb16-34">    </span>
<span id="cb16-35">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> best_aggregate(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb16-36">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Return the prompt with highest aggregate score."""</span></span>
<span id="cb16-37">        aggregates <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(s) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> s <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.scores]</span>
<span id="cb16-38">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prompts[np.argmax(aggregates)]</span></code></pre></div></div>
<hr>
</section>
<section id="putting-it-together-pareto-guided-optimization" class="level4">
<h4 class="anchored" data-anchor-id="putting-it-together-pareto-guided-optimization">Putting It Together: Pareto-Guided Optimization</h4>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb17-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> optimize_with_pareto(seed_prompt, traindf, valdf, n_iters<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, mb_size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>):</span>
<span id="cb17-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Reflective mutation with Pareto frontier selection."""</span></span>
<span id="cb17-3">    frontier <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ParetoFrontier()</span>
<span id="cb17-4">    </span>
<span id="cb17-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Initialize with seed</span></span>
<span id="cb17-6">    seed_scores <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> evaluate_per_instance(seed_prompt, valdf)</span>
<span id="cb17-7">    frontier.add(seed_prompt, seed_scores)</span>
<span id="cb17-8">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'Baseline: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(seed_scores)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(seed_scores)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.1%}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span>)</span>
<span id="cb17-9">    </span>
<span id="cb17-10">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(n_iters):</span>
<span id="cb17-11">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'='</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">40</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">Iteration </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'='</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">40</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb17-12">        </span>
<span id="cb17-13">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Sample parent from frontier (weighted by unique wins)</span></span>
<span id="cb17-14">        parent <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> frontier.sample()</span>
<span id="cb17-15">        </span>
<span id="cb17-16">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Run on minibatch, reflect, propose mutation</span></span>
<span id="cb17-17">        mb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> traindf.sample(mb_size)</span>
<span id="cb17-18">        mb_results <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> evaluate_with_traces(parent, mb)</span>
<span id="cb17-19">        new_prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> reflect_and_mutate(parent, mb_results)</span>
<span id="cb17-20">        </span>
<span id="cb17-21">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Evaluate on full validation set</span></span>
<span id="cb17-22">        new_scores <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> evaluate_per_instance(new_prompt, valdf)</span>
<span id="cb17-23">        new_agg <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(new_scores) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(new_scores)</span>
<span id="cb17-24">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"New prompt: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>new_agg<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.1%}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> aggregate"</span>)</span>
<span id="cb17-25">        </span>
<span id="cb17-26">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Try to add to frontier</span></span>
<span id="cb17-27">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> frontier.add(new_prompt, new_scores):</span>
<span id="cb17-28">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"✓ Added to frontier (size: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(frontier.prompts)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">)"</span>)</span>
<span id="cb17-29">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span>:</span>
<span id="cb17-30">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"✗ Dominated, rejected"</span>)</span>
<span id="cb17-31">    </span>
<span id="cb17-32">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> frontier.best_aggregate()</span></code></pre></div></div>
<hr>
</section>
<section id="what-we-observed-on-aime" class="level4">
<h4 class="anchored" data-anchor-id="what-we-observed-on-aime">What We Observed on AIME</h4>
<p>Running this on AIME problems with Gemini 2.5 Flash:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Iteration</th>
<th>Aggregate</th>
<th>Instances Solved</th>
<th>Frontier Action</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Seed</td>
<td>10%</td>
<td>#0</td>
<td>Initialize</td>
</tr>
<tr class="even">
<td>1</td>
<td>10%</td>
<td>#0</td>
<td>Dominated by seed, rejected</td>
</tr>
<tr class="odd">
<td>2</td>
<td>30%</td>
<td>#0, #1, #2</td>
<td>Added, dominates seed</td>
</tr>
<tr class="even">
<td>3</td>
<td>10%</td>
<td>#9 only</td>
<td><strong>Added</strong> ✓ (unique win on #9)</td>
</tr>
</tbody>
</table>
<p><strong>The key moment</strong>: Iteration 3 scored only 10%—worse than iteration 2’s 30%. Greedy selection would discard it entirely.</p>
<p>But it solved <strong>instance #9</strong>, which nothing else could. Pareto selection preserves it.</p>
<p>Our final frontier: <strong>{P2, P3}</strong></p>
<ul>
<li>P2: Strong generalist (30%), knows systems-of-equations strategies</li>
<li>P3: Instance-9 specialist (10%), knows whatever cracked that specific problem</li>
</ul>
<p>Both insights survive. The merge operation (covered later) can combine them.</p>
<hr>
</section>
<section id="why-this-matters-no-more-catastrophic-forgetting" class="level4">
<h4 class="anchored" data-anchor-id="why-this-matters-no-more-catastrophic-forgetting">Why This Matters: No More Catastrophic Forgetting</h4>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Selection Strategy</th>
<th>What happens to specialists</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Greedy</strong></td>
<td>Discarded whenever aggregate score drops</td>
</tr>
<tr class="even">
<td><strong>Pareto</strong></td>
<td>Preserved if they solve <em>anything</em> unique</td>
</tr>
</tbody>
</table>
<p>Greedy selection caused our iteration 3 collapse—the number-theory prompt overwrote the algebra prompt’s insights. Pareto selection prevents this by construction: you can’t remove a prompt from the frontier unless something else does everything it does, <em>plus more</em>.</p>
<p>This drives GEPA’s sample efficiency. Instead of needing thousands of examples to statistically rediscover lost insights, the frontier retains them automatically.</p>
<hr>
<p>Pareto selection preserved iteration 3’s prompt despite its 10% aggregate score—because it solved instance #9, which nothing else could. The intuition for why keeping “losers” helps comes from <strong>quality-diversity</strong> algorithms in evolutionary computation.</p>
</section>
</section>
<section id="why-pareto-works-quality-diversity-and-map-elites" class="level3">
<h3 class="anchored" data-anchor-id="why-pareto-works-quality-diversity-and-map-elites">Why Pareto Works: Quality-Diversity and Map Elites</h3>
<p>We’ve now built both core mechanisms from scratch—reflective mutation and Pareto selection. Before diving into the full algorithm, let’s step back and understand <em>why</em> this approach works so well.</p>
<p>The Pareto frontier isn’t a novel invention—it draws from <strong>quality-diversity (QD)</strong> algorithms, a family of techniques from evolutionary computation that work well when diversity itself is valuable.</p>
<p>Traditional optimization asks: <em>“What’s the single best solution?”</em></p>
<p>Quality-diversity asks: <em>“What’s the best solution of each <em>type</em>?”</em></p>
<p>Complex problems often have multiple valid approaches. A chess engine that always plays aggressively might beat one that always plays defensively—but the best engine knows <em>both</em> styles and picks situationally. Maintaining diverse specialists, then combining their insights, outperforms converging prematurely on one “best” approach.</p>
<section id="map-elites" class="level4">
<h4 class="anchored" data-anchor-id="map-elites">Map Elites</h4>
<p><a href="https://arxiv.org/abs/1504.04909">Map Elites</a> (Mouret &amp; Clune, 2015) maintains an <em>archive</em> organized by behavior:</p>
<ol type="1">
<li><strong>Define behavior dimensions</strong> — characteristics describing <em>how</em> a solution works (not just how well)</li>
<li><strong>Discretize into bins</strong> — each cell represents a “niche”</li>
<li><strong>Keep the best per bin</strong> — new solutions compete only within their niche</li>
<li><strong>Mutate from the archive</strong> — sample from any occupied bin, mutate, place in appropriate bin</li>
</ol>
<p>The result: diverse specialists, each optimal <em>of its type</em>. The archive provides <em>stepping stones</em>: a mutation from one niche might discover something useful for another. Diversity doubles as a search strategy.</p>
</section>
<section id="gepas-adaptation-validation-instances-as-niches" class="level4">
<h4 class="anchored" data-anchor-id="gepas-adaptation-validation-instances-as-niches">GEPA’s Adaptation: Validation Instances as Niches</h4>
<p>GEPA recognizes that <strong>the validation set itself defines the behavior space</strong>:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 66%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th>Map Elites</th>
<th>GEPA</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Behavior = continuous dimensions</td>
<td>Behavior = which validation instances are solved</td>
</tr>
<tr class="even">
<td>Bins = discretized regions</td>
<td>“Bins” = individual validation instances</td>
</tr>
<tr class="odd">
<td>Archive = best per bin</td>
<td>Pareto frontier = non-dominated prompts across instances</td>
</tr>
</tbody>
</table>
<p>Each validation instance encodes what domain-specific insights are required. A prompt solving only instance #7 is the “specialist for that niche”—it stays because it demonstrates <em>something works</em>, even with low aggregate score.</p>
<blockquote class="blockquote">
<p><em>“By tracking the best-performing candidate on each validation instance, GEPA can maintain all the domain-specific insights that solve any of these problems.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p>This motivated GEPA’s design: Pareto selection over per-instance scores naturally implements QD’s “best per niche” principle, while the merge operation recombines insights across niches—exactly what stepping-stone search requires.</p>
<p>With both mechanisms in place, there’s one more operation that makes GEPA powerful: <strong>merge</strong>—combining insights from divergent lineages.</p>
</section>
</section>
<section id="the-lineage-tree-and-system-aware-merge" class="level3">
<h3 class="anchored" data-anchor-id="the-lineage-tree-and-system-aware-merge">The Lineage Tree and System-Aware Merge</h3>
<p>Pareto selection preserves specialists—but it creates a new problem: insights get siloed in separate branches.</p>
<p>Consider what happens after 10 iterations of GEPA on AIME problems:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/gepa-deepdive/static/Merge_Visual.webp" class="img-fluid figure-img"></p>
<figcaption>Lineage Tree with Merge Operation</figcaption>
</figure>
</div>
<p>The P2→P4 lineage accumulated <strong>algebra insights</strong>. The P3→P5 lineage accumulated <strong>number theory insights</strong>. Both survive on the Pareto frontier because each solves problems the other can’t.</p>
<p>But what about a problem requiring <em>both</em>?</p>
<hr>
<section id="the-recombination-problem" class="level4">
<h4 class="anchored" data-anchor-id="the-recombination-problem">The Recombination Problem</h4>
<p>Suppose validation instance #7 needs algebraic manipulation to set up a system of equations, then modular arithmetic to constrain the solution space. Neither P4 nor P5 can solve it:</p>
<ul>
<li>P4 knows to subtract equations pairwise, but doesn’t think to reduce mod p</li>
<li>P5 knows CRT, but misses the algebraic setup</li>
</ul>
<p>With mutation alone, P4 would need to <em>independently rediscover</em> number theory (which P5 already knows), or vice versa. The knowledge exists in our population—just in different branches.</p>
<p>Merge addresses this by combining insights from divergent lineages into a single candidate.</p>
<hr>
</section>
<section id="how-merge-works" class="level4">
<h4 class="anchored" data-anchor-id="how-merge-works">How Merge Works</h4>
<p>GEPA’s merge is “system-aware”—the LLM understands what it’s combining, so it can resolve contradictions and synthesize coherently rather than blindly concatenating (unlike genetic algorithm crossover which swaps segments randomly). The reflection LLM receives:</p>
<ol type="1">
<li><strong>Both parent prompts</strong> with their full instruction text</li>
<li><strong>Lineage context</strong> — what types of problems each lineage solved</li>
<li><strong>Conflict guidance</strong> — instructions to resolve contradictions, not ignore them</li>
</ol>
<p>The prompt asks the LLM to <em>synthesize</em>, not concatenate:</p>
<blockquote class="blockquote">
<p>“Create a SINGLE unified instruction set that incorporates key insights from BOTH. Preserve specific strategies. Resolve contradictions thoughtfully. Don’t simply concatenate—synthesize into coherent guidance.”</p>
</blockquote>
<p><strong>Concrete example</strong> — merging our AIME specialists:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 22%">
<col style="width: 30%">
<col style="width: 47%">
</colgroup>
<thead>
<tr class="header">
<th>Parent</th>
<th>Specialty</th>
<th>Key instruction</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>P4</td>
<td>Algebra</td>
<td>“Subtract equations pairwise to expose (x-z)(y-A)=0. Enumerate all cases.”</td>
</tr>
<tr class="even">
<td>P5</td>
<td>Number theory</td>
<td>“Apply CRT for modular constraints. Check for contradictions.”</td>
</tr>
</tbody>
</table>
<p><strong>Merged offspring P6:</strong></p>
<blockquote class="blockquote">
<p>“Approach: (1) For equation systems, subtract pairwise to expose factor relationships—enumerate all cases including edge cases. (2) When modular conditions appear, apply CRT and check for contradictions. (3) Competition problems often combine both: set up the algebra first, then use number-theoretic constraints to eliminate impossible cases.”</p>
</blockquote>
<p>P6 inherits both toolkits and adds meta-knowledge about when to combine them. This is the “system-aware” part—the merge understands the semantics of what it’s combining.</p>
<hr>
</section>
<section id="when-to-merge-vs.-mutate" class="level4">
<h4 class="anchored" data-anchor-id="when-to-merge-vs.-mutate">When to Merge vs.&nbsp;Mutate</h4>
<p>GEPA alternates between operations based on frontier state:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 33%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th>Condition</th>
<th>Operation</th>
<th>Rationale</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Early iterations (&lt; 5)</td>
<td>Mutate</td>
<td>Let lineages diverge first; nothing to merge yet</td>
</tr>
<tr class="even">
<td>Frontier has one dominant lineage</td>
<td>Mutate</td>
<td>No orthogonal insights to combine</td>
</tr>
<tr class="odd">
<td>Frontier has divergent specialists</td>
<td>Merge</td>
<td>Recombine discoveries from parallel explorations</td>
</tr>
<tr class="even">
<td>Recent merge succeeded</td>
<td>Mutate</td>
<td>Refine the merged candidate</td>
</tr>
</tbody>
</table>
<p>The paper describes the decision:</p>
<blockquote class="blockquote">
<p><em>“As we run GEPA for longer, an evolutionary tree appears where different lineages can have different insights gathered into them. System-aware merge tries to merge two different lineages to encapsulate the insights gathered in them into a single candidate.”</em> — Lakshya A Agrawal</p>
</blockquote>
<hr>
</section>
<section id="merge-vs.-genetic-algorithm-crossover" class="level4">
<h4 class="anchored" data-anchor-id="merge-vs.-genetic-algorithm-crossover">Merge vs.&nbsp;Genetic Algorithm Crossover</h4>
<p>GEPA’s merge is analogous to crossover in genetic algorithms, but smarter:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 62%">
<col style="width: 37%">
</colgroup>
<thead>
<tr class="header">
<th>Genetic Algorithms</th>
<th>GEPA Merge</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Genes = bit positions</td>
<td>Insights = natural language instructions</td>
</tr>
<tr class="even">
<td>Crossover = swap bit segments randomly</td>
<td>Merge = LLM synthesizes with understanding</td>
</tr>
<tr class="odd">
<td>Can create invalid offspring</td>
<td>Can resolve contradictions</td>
</tr>
<tr class="even">
<td>Blind to semantics</td>
<td>Aware of what instructions <em>mean</em></td>
</tr>
</tbody>
</table>
<p>Random crossover might produce: “Subtract equations pairwise. Apply CRT. Subtract equations pairwise.” (nonsense duplication)</p>
<p>LLM merge produces: “Set up algebra first, then apply number-theoretic constraints.” (coherent synthesis)</p>
<hr>
</section>
<section id="how-the-pieces-fit" class="level4">
<h4 class="anchored" data-anchor-id="how-the-pieces-fit">How the Pieces Fit</h4>
<p>Pareto selection preserves the diversity that makes merge valuable in the first place. Lineage tracking tells us which candidates come from divergent branches. Merge recombines their discoveries, and then Pareto selection preserves successful merges alongside the remaining specialists.</p>
<p>If you only kept one “best” prompt, there’d be nothing interesting to merge. And without merge, insights stay siloed even when the frontier is diverse.</p>
<blockquote class="blockquote">
<p><strong>Implementation note</strong>: The full GEPA implementation includes safeguards to ensure merge candidates actually have different insights worth combining (checking for common ancestors, avoiding redundant merges, verifying that descendants improved on their ancestor). See the <a href="https://github.com/stanfordnlp/dspy">DSPy source</a> for details.</p>
</blockquote>
<p>Mutation refines a single lineage through reflection. Merge recombines what parallel lineages discovered. Pareto selection preserves both.</p>
<p>With reflective mutation, Pareto selection, and merge all in place, here’s how they combine into GEPA’s full optimization loop.</p>
</section>
</section>
<section id="the-complete-algorithm" class="level3">
<h3 class="anchored" data-anchor-id="the-complete-algorithm">The Complete Algorithm</h3>
<p>Here’s how the pieces combine into the full optimization loop.</p>
<section id="algorithm-overview" class="level4">
<h4 class="anchored" data-anchor-id="algorithm-overview">Algorithm Overview</h4>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb18-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> random</span>
<span id="cb18-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb18-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> typing <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Callable</span>
<span id="cb18-4"></span>
<span id="cb18-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> gepa(</span>
<span id="cb18-6">    base_prompt: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>,</span>
<span id="cb18-7">    trainset: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>,</span>
<span id="cb18-8">    valset: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>,</span>
<span id="cb18-9">    evaluate_fn: Callable,          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># (prompt, example) -&gt; score (0 or 1)</span></span>
<span id="cb18-10">    run_with_feedback_fn: Callable, <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># (prompt, examples) -&gt; (traces: list[str], feedback: list[str])</span></span>
<span id="cb18-11">    reflect_fn: Callable,           <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># (parent_prompt, traces, feedback) -&gt; new_prompt: str</span></span>
<span id="cb18-12">    merge_fn: Callable,             <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># (prompt1, prompt2) -&gt; merged_prompt: str</span></span>
<span id="cb18-13">    max_iterations: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>,</span>
<span id="cb18-14">    minibatch_size: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>,</span>
<span id="cb18-15">):</span>
<span id="cb18-16">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb18-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    GEPA: Genetic-Pareto prompt optimization.</span></span>
<span id="cb18-18"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    </span></span>
<span id="cb18-19"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    Returns the best aggregate prompt; access full frontier via returned dict.</span></span>
<span id="cb18-20"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    """</span></span>
<span id="cb18-21">    </span>
<span id="cb18-22">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- Helper functions ---</span></span>
<span id="cb18-23">    </span>
<span id="cb18-24">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> evaluate_all(prompt, dataset):</span>
<span id="cb18-25">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Return per-instance scores as list."""</span></span>
<span id="cb18-26">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> [evaluate_fn(prompt, ex) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> ex <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> dataset]</span>
<span id="cb18-27">    </span>
<span id="cb18-28">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> evaluate_minibatch(prompt, minibatch):</span>
<span id="cb18-29">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Return aggregate score on minibatch."""</span></span>
<span id="cb18-30">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(evaluate_fn(prompt, ex) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> ex <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> minibatch) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(minibatch)</span>
<span id="cb18-31">    </span>
<span id="cb18-32">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> dominates(scores_a, scores_b):</span>
<span id="cb18-33">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Does A dominate B? (&gt;= everywhere, &gt; somewhere)"""</span></span>
<span id="cb18-34">        a, b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array(scores_a), np.array(scores_b)</span>
<span id="cb18-35">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> b).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">all</span>() <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">and</span> (a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> b).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">any</span>()</span>
<span id="cb18-36">    </span>
<span id="cb18-37">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> is_dominated_by_frontier(new_scores, frontier, scores):</span>
<span id="cb18-38">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Is new_scores dominated by ANY frontier member?"""</span></span>
<span id="cb18-39">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">any</span>(dominates(scores[c], new_scores) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> frontier)</span>
<span id="cb18-40">    </span>
<span id="cb18-41">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> sample_from_frontier(frontier, scores):</span>
<span id="cb18-42">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Sample weighted by unique wins."""</span></span>
<span id="cb18-43">        n_instances <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">iter</span>(scores.values())))</span>
<span id="cb18-44">        weights <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb18-45">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> candidate <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> frontier:</span>
<span id="cb18-46">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(frontier) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>:</span>
<span id="cb18-47">                unique_wins <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> n_instances</span>
<span id="cb18-48">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span>:</span>
<span id="cb18-49">                others_max <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([scores[o] <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> frontier <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> o <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> candidate]).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">max</span>(axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb18-50">                unique_wins <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (np.array(scores[candidate]) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> others_max).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>()</span>
<span id="cb18-51">            weights.append(unique_wins <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># +1 smoothing</span></span>
<span id="cb18-52">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> random.choices(frontier, weights<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>weights)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb18-53">    </span>
<span id="cb18-54">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> get_root(prompt, lineage):</span>
<span id="cb18-55">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Trace lineage back to root."""</span></span>
<span id="cb18-56">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> lineage.get(prompt) <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">is</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>:</span>
<span id="cb18-57">            prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> lineage[prompt]</span>
<span id="cb18-58">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> prompt</span>
<span id="cb18-59">    </span>
<span id="cb18-60">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> should_merge(frontier, lineage, iteration):</span>
<span id="cb18-61">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Decide whether to merge or mutate."""</span></span>
<span id="cb18-62">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(frontier) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">or</span> iteration <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>:</span>
<span id="cb18-63">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span></span>
<span id="cb18-64">        n_lineages <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>(get_root(c, lineage) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> frontier))</span>
<span id="cb18-65">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> random.random() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> (n_lineages <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(frontier)</span>
<span id="cb18-66">    </span>
<span id="cb18-67">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> select_divergent(frontier, parent, lineage):</span>
<span id="cb18-68">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Select a candidate from a different lineage."""</span></span>
<span id="cb18-69">        parent_root <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> get_root(parent, lineage)</span>
<span id="cb18-70">        others <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [c <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> frontier <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> get_root(c, lineage) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> parent_root]</span>
<span id="cb18-71">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> random.choice(others) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> others <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> random.choice(frontier)</span>
<span id="cb18-72">    </span>
<span id="cb18-73">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> best_aggregate(frontier, scores):</span>
<span id="cb18-74">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Return prompt with highest aggregate score."""</span></span>
<span id="cb18-75">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">max</span>(frontier, key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> c: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(scores[c]))</span>
<span id="cb18-76">    </span>
<span id="cb18-77">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- Main loop ---</span></span>
<span id="cb18-78">    </span>
<span id="cb18-79">    candidates <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [base_prompt]</span>
<span id="cb18-80">    scores <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {base_prompt: evaluate_all(base_prompt, valset)}</span>
<span id="cb18-81">    pareto_frontier <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [base_prompt]</span>
<span id="cb18-82">    lineage <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {base_prompt: <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>}</span>
<span id="cb18-83">    </span>
<span id="cb18-84">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> iteration <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, max_iterations <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>):</span>
<span id="cb18-85">        </span>
<span id="cb18-86">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. SAMPLE: Select parent from Pareto frontier</span></span>
<span id="cb18-87">        parent <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sample_from_frontier(pareto_frontier, scores)</span>
<span id="cb18-88">        </span>
<span id="cb18-89">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. PROPOSE: Either mutate or merge</span></span>
<span id="cb18-90">        minibatch <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> random.sample(trainset, <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">min</span>(minibatch_size, <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(trainset)))</span>
<span id="cb18-91">        </span>
<span id="cb18-92">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> should_merge(pareto_frontier, lineage, iteration):</span>
<span id="cb18-93">            other_parent <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> select_divergent(pareto_frontier, parent, lineage)</span>
<span id="cb18-94">            new_prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> merge_fn(parent, other_parent)</span>
<span id="cb18-95">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span>:</span>
<span id="cb18-96">            traces, feedback <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> run_with_feedback_fn(parent, minibatch)</span>
<span id="cb18-97">            new_prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> reflect_fn(parent, traces, feedback)</span>
<span id="cb18-98">        </span>
<span id="cb18-99">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. EVALUATE: Mini-batch gate</span></span>
<span id="cb18-100">        parent_mb_score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> evaluate_minibatch(parent, minibatch)</span>
<span id="cb18-101">        new_mb_score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> evaluate_minibatch(new_prompt, minibatch)</span>
<span id="cb18-102">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> new_mb_score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> parent_mb_score:</span>
<span id="cb18-103">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Reject: didn't improve on mini-batch</span></span>
<span id="cb18-104">        </span>
<span id="cb18-105">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Full evaluation</span></span>
<span id="cb18-106">        new_scores <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> evaluate_all(new_prompt, valset)</span>
<span id="cb18-107">        </span>
<span id="cb18-108">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 4. UPDATE: Pareto frontier maintenance</span></span>
<span id="cb18-109">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> is_dominated_by_frontier(new_scores, pareto_frontier, scores):</span>
<span id="cb18-110">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Reject: dominated by existing candidate</span></span>
<span id="cb18-111">        </span>
<span id="cb18-112">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Remove dominated candidates</span></span>
<span id="cb18-113">        pareto_frontier <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [c <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> pareto_frontier </span>
<span id="cb18-114">                          <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> dominates(new_scores, scores[c])]</span>
<span id="cb18-115">        </span>
<span id="cb18-116">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add new candidate</span></span>
<span id="cb18-117">        candidates.append(new_prompt)</span>
<span id="cb18-118">        scores[new_prompt] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> new_scores</span>
<span id="cb18-119">        pareto_frontier.append(new_prompt)</span>
<span id="cb18-120">        lineage[new_prompt] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> parent</span>
<span id="cb18-121">    </span>
<span id="cb18-122">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> {</span>
<span id="cb18-123">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'best'</span>: best_aggregate(pareto_frontier, scores),</span>
<span id="cb18-124">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'frontier'</span>: pareto_frontier,</span>
<span id="cb18-125">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'scores'</span>: scores,</span>
<span id="cb18-126">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'lineage'</span>: lineage,</span>
<span id="cb18-127">    }</span></code></pre></div></div>
<hr>
</section>
<section id="the-key-decision-points" class="level4">
<h4 class="anchored" data-anchor-id="the-key-decision-points">The Key Decision Points</h4>
<p><strong>1. Candidate Sampling</strong> — Weighted by unique wins, so specialists get attention proportional to their unique value.</p>
<p><strong>2. Mutation vs Merge</strong> — Early iterations favor mutation; merge probability increases as frontier diversifies.</p>
<p><strong>3. Mini-Batch Gating</strong></p>
<p>Before expensive full evaluation, GEPA checks if the new candidate improves on the mini-batch it was designed to fix. This saves compute on obviously bad mutations:</p>
<blockquote class="blockquote">
<p><em>“We propose one new candidate, do a mini-batch evaluation to see whether this new candidate improves on this mini-batch or not. And if it does improve, then we track the score for this new candidate on all validation instances.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p><strong>4. Pareto Update</strong></p>
<p>The frontier update follows the dominance logic we implemented earlier:</p>
<ul>
<li><strong>Reject</strong> new candidates dominated by existing ones (they add nothing)</li>
<li><strong>Remove</strong> existing candidates dominated by the new one (they’re obsolete)</li>
<li><strong>Keep</strong> all non-dominated candidates (each offers unique value)</li>
</ul>
<hr>
</section>
<section id="complexity-analysis" class="level4">
<h4 class="anchored" data-anchor-id="complexity-analysis">Complexity Analysis</h4>
<table class="caption-top table">
<colgroup>
<col style="width: 64%">
<col style="width: 35%">
</colgroup>
<thead>
<tr class="header">
<th>Operation</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Mutation (3-4 rollouts + reflection)</td>
<td>3-4 LLM calls + 1 reflection call</td>
</tr>
<tr class="even">
<td>Mini-batch evaluation</td>
<td>3-4 metric evaluations</td>
</tr>
<tr class="odd">
<td>Full validation evaluation</td>
<td>N metric evaluations (N = valset size)</td>
</tr>
<tr class="even">
<td>Pareto check</td>
<td>O(F × N) comparisons (F = frontier size)</td>
</tr>
</tbody>
</table>
<p>The mini-batch gate matters because most mutations fail—they either don’t improve on their target examples or regress elsewhere. Catching failures early (3 evaluations) rather than late (N evaluations) saves ~90% of evaluation budget on rejected candidates.</p>
<hr>
</section>
<section id="why-each-component-matters" class="level4">
<h4 class="anchored" data-anchor-id="why-each-component-matters">Why Each Component Matters</h4>
<table class="caption-top table">
<colgroup>
<col style="width: 34%">
<col style="width: 37%">
<col style="width: 28%">
</colgroup>
<thead>
<tr class="header">
<th>Component</th>
<th>Without it</th>
<th>With it</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Textual feedback</strong></td>
<td>Optimizer sees only <code>score=0.6</code></td>
<td>Optimizer reads “wrong answer, expected X, got Y because…”</td>
</tr>
<tr class="even">
<td><strong>Pareto selection</strong></td>
<td>Specialists discarded when aggregate drops</td>
<td>Specialists preserved if they solve anything unique</td>
</tr>
<tr class="odd">
<td><strong>Lineage tracking</strong></td>
<td>No memory of evolutionary history</td>
<td>Can identify divergent branches for merge</td>
</tr>
<tr class="even">
<td><strong>Merge operation</strong></td>
<td>Insights stay siloed in separate branches</td>
<td>Orthogonal discoveries can combine</td>
</tr>
<tr class="odd">
<td><strong>Mini-batch gating</strong></td>
<td>Evaluate every candidate fully</td>
<td>Reject obvious failures cheaply</td>
</tr>
</tbody>
</table>
<p>These interact: Pareto selection preserves the diversity that makes merge valuable, textual feedback makes both mutation and merge more effective, and mini-batch gating keeps evaluation costs reasonable.</p>
<hr>
</section>
<section id="what-gets-returned" class="level4">
<h4 class="anchored" data-anchor-id="what-gets-returned">What Gets Returned</h4>
<p>The algorithm returns <code>best_aggregate(pareto_frontier)</code>—the prompt with highest overall validation score. But for analysis, the full frontier is valuable: in DSPy, use <code>track_stats=True</code> to access all candidates and their per-instance scores.</p>
<hr>
<p>That’s the full loop. Now let’s see what the optimized prompts actually look like.</p>
</section>
</section>
<section id="what-gepa-learns-domain-specific-knowledge-encoding" class="level3">
<h3 class="anchored" data-anchor-id="what-gepa-learns-domain-specific-knowledge-encoding">What GEPA Learns: Domain-Specific Knowledge Encoding</h3>
<p>We’ve built the full algorithm. What does all this machinery actually <em>produce</em>? The output is prompts that encode domain expertise.</p>
<p>GEPA can <strong>encode domain-specific knowledge directly into prompts</strong>—turning what experts know implicitly into explicit instructions that persist across examples.</p>
<section id="prompts-as-knowledge-containers" class="level4">
<h4 class="anchored" data-anchor-id="prompts-as-knowledge-containers">Prompts as Knowledge Containers</h4>
<p>Traditional optimization treats prompts as opaque strings to be scored. GEPA treats them as <strong>knowledge containers</strong> that accumulate insights through the reflection loop:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/gepa-deepdive/static/failure_acc_experience.webp" class="img-fluid figure-img"></p>
<figcaption>Failure Accumulation Experience</figcaption>
</figure>
</div>
<p>Each iteration doesn’t just fix one error—it extracts the <em>lesson</em> behind the error.</p>
<p><strong>Concrete example from our AIME experiments:</strong></p>
<table class="caption-top table">
<colgroup>
<col style="width: 30%">
<col style="width: 69%">
</colgroup>
<thead>
<tr class="header">
<th>Stage</th>
<th>Prompt excerpt</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Seed</strong></td>
<td>“You are given a problem and you have to give the answer along with reasoning.”</td>
</tr>
<tr class="even">
<td><strong>After iter 2</strong></td>
<td>“…For systems like xy+Az=C, yz+Ax=C, zx+Ay=C, subtract equations pairwise to expose factor relationships like (x-z)(y-A)=0. Enumerate all cases including x=z and y=A.”</td>
</tr>
<tr class="odd">
<td><strong>After iter 3</strong></td>
<td>“…When remainders appear (n mod x, n mod y), check for contradictions via modular arithmetic. An answer of 0 often indicates impossible constraints.”</td>
</tr>
</tbody>
</table>
<p>The prompt evolved from generic instruction to encoding <strong>competition math heuristics</strong>. The LLM didn’t invent these—it extracted them from its training knowledge, triggered by seeing specific failure modes.</p>
</section>
<section id="three-categories-of-captured-knowledge" class="level4">
<h4 class="anchored" data-anchor-id="three-categories-of-captured-knowledge">Three Categories of Captured Knowledge</h4>
<p>We observe GEPA capturing different types of domain expertise (our interpretive taxonomy, not from the paper):</p>
<p><strong>1. Format and Interface Knowledge</strong> - Output schemas (“return JSON with keys: answer, reasoning”) - API conventions (“use library.method(), not library_method()”) - Parsing requirements (“integers only, no leading zeros”)</p>
<p>This is easiest to extract since format errors produce explicit feedback.</p>
<p><strong>2. Strategic Knowledge</strong> - Problem-solving heuristics (“try small cases first”) - Domain patterns (“competition problems often combine algebra and number theory”) - Failure mode awareness (“watch for off-by-one errors in counting”)</p>
<p>This emerges from reflecting on <em>why</em> approaches failed, not just <em>that</em> they failed.</p>
<p><strong>3. Factual Domain Knowledge</strong> - API names and signatures (“torch.einsum, not torch.einstein_sum”) - Domain constants (“standard gravity = 9.81 m/s²”) - Constraint relationships (“in valid Sudoku, each row/column/box contains 1-9 exactly once”)</p>
<p>The LLM already knows this—reflection surfaces it into the prompt where it’s consistently applied.</p>
</section>
<section id="why-prompts-beat-weights-sometimes" class="level4">
<h4 class="anchored" data-anchor-id="why-prompts-beat-weights-sometimes">Why Prompts Beat Weights (Sometimes)</h4>
<p>Fine-tuning encodes knowledge in model weights—opaque, distributed, hard to inspect. GEPA encodes knowledge in natural language—readable and editable.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Aspect</th>
<th>Fine-tuning</th>
<th>GEPA prompts</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Inspectability</strong></td>
<td>Black box</td>
<td>Human-readable instructions</td>
</tr>
<tr class="even">
<td><strong>Editability</strong></td>
<td>Requires retraining</td>
<td>Edit the text directly</td>
</tr>
<tr class="odd">
<td><strong>Composability</strong></td>
<td>Train new model</td>
<td>Merge prompt sections</td>
</tr>
<tr class="even">
<td><strong>Sample efficiency</strong></td>
<td>Thousands of examples</td>
<td>Tens of examples</td>
</tr>
</tbody>
</table>
<p>A GEPA-optimized prompt can be read by a human to understand what strategies it learned. You can edit it to add domain knowledge the optimizer missed, or transfer it to different LLMs.</p>
</section>
<section id="the-preservation-problem" class="level4">
<h4 class="anchored" data-anchor-id="the-preservation-problem">The Preservation Problem</h4>
<p>But new insights can overwrite old ones. We saw this in our hands-on experiment when iteration 3’s number theory insights overwrote iteration 2’s algebra insights, causing catastrophic forgetting.</p>
<p>Pareto selection prevents this. By preserving prompts that are best on <em>any</em> validation instance, it keeps specialized knowledge around even when aggregate scores dip. The algebra specialist and number theory specialist both stay on the frontier, and the merge operation can later combine their insights.</p>
<p>Without this, GEPA would periodically erase its own discoveries.</p>
</section>
<section id="limitation-knowledge-must-be-triggerable" class="level4">
<h4 class="anchored" data-anchor-id="limitation-knowledge-must-be-triggerable">Limitation: Knowledge Must Be Triggerable</h4>
<p>GEPA can only surface knowledge the base LLM already has. It’s extraction, not creation. Stronger base models yield better results because there’s more latent knowledge to work with. For specialized domains (custom hardware APIs, proprietary protocols), you may need human-written seed instructions or few-shot examples to get the reflection loop started.</p>
<hr>
<p>So far we’ve focused on <em>training</em>: optimize prompts on labeled examples, deploy the best one. But GEPA’s machinery—reflective mutation, Pareto selection, merge—can also run at <em>inference time</em> as a search algorithm.</p>
</section>
</section>
</section>
<section id="beyond-training-gepa-for-inference-time-search" class="level2">
<h2 class="anchored" data-anchor-id="beyond-training-gepa-for-inference-time-search">Beyond Training: GEPA for Inference-Time Search</h2>
<p>Everything we’ve covered so far assumes a familiar workflow: optimize on training data, deploy the result on new tasks. But GEPA supports a second paradigm that flips this on its head.</p>
<section id="two-paradigms-of-operation" class="level3">
<h3 class="anchored" data-anchor-id="two-paradigms-of-operation">Two Paradigms of Operation</h3>
<p><strong>Train-then-generalize</strong> (what we’ve built so far):</p>
<ul>
<li>Optimize prompts on a training set</li>
<li>Select the best-aggregate prompt from the Pareto frontier</li>
<li>Deploy that prompt on new, unseen tasks</li>
<li>Goal: learn <em>generalizable</em> lessons that transfer</li>
</ul>
<p><strong>Test-time search</strong> (inference-time optimization):</p>
<ul>
<li>You have a batch of hard tasks you need to solve <em>now</em></li>
<li>Optimize directly on the tasks themselves</li>
<li>GEPA searches for solutions, storing the best prompt <em>per task</em></li>
<li>Goal: maximize performance on <em>these specific instances</em></li>
</ul>
<p>The mechanics are identical—reflective mutation, Pareto selection, merge. What changes is the intent: instead of learning transferable knowledge, you’re using GEPA as a <strong>search algorithm</strong> over the solution space. This is an instance of <a href="https://openai.com/index/learning-to-reason-with-llms/">inference-time compute scaling</a>—investing more computation at inference to solve harder problems.</p>
<p><strong>The key mechanical change</strong>: In normal GEPA, you have separate <code>trainset</code> (to learn from via reflection) and <code>valset</code> (to evaluate generalization). The Pareto frontier tracks per-instance performance on <code>valset</code>, preserving prompts that generalize well.</p>
<p>For test-time search, pass the <em>same</em> problems as both <code>trainset</code> and <code>valset</code>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb19-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Test-time search: optimize directly on the tasks you want to solve</span></span>
<span id="cb19-2">optimized <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> GEPA(metric<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>metric_with_feedback, auto<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"medium"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">compile</span>(</span>
<span id="cb19-3">    program,</span>
<span id="cb19-4">    trainset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>hard_problems,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># The actual tasks you need solved</span></span>
<span id="cb19-5">    valset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>hard_problems,    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Same held-out validation</span></span>
<span id="cb19-6">)</span></code></pre></div></div>
<p>This tells GEPA: “I don’t care about generalization—optimize directly on <em>these specific problems</em>.” The Pareto frontier now tracks “best prompt for each problem” rather than “prompts that transfer to unseen data.” See the <a href="https://dspy.ai/api/optimizers/GEPA/overview/">GEPA API documentation</a> for full parameter details.</p>
<blockquote class="blockquote">
<p><em>“Given a batch of tasks that we want to solve and given some budget… GEPA can propose and update its own strategy to solve that particular task iteratively till that task is solved.”</em> — Lakshya A Agrawal</p>
</blockquote>
<hr>
</section>
<section id="why-gepa-beats-high-temperature-sampling" class="level3">
<h3 class="anchored" data-anchor-id="why-gepa-beats-high-temperature-sampling">Why GEPA Beats High-Temperature Sampling</h3>
<p>Traditional inference-time strategies sample at high temperature to generate many candidates, then pick the best. But these samples tend to be <em>similar</em>—variations on the same approach. GEPA induces <strong>genuine diversity</strong> through Pareto tracking (maintaining candidates that excel at <em>something different</em>) and reflective mutation (proposing <em>structurally different</em> strategies based on what went wrong, not random perturbations).</p>
<p>When feedback says “memory bandwidth bottleneck,” the next candidate might switch from a naive loop to shared-memory tiling—a qualitative change that temperature variation rarely discovers. On the <a href="https://arxiv.org/abs/2103.03874">MATH benchmark</a>, this approach achieves <strong>93% accuracy</strong> compared to 67% with basic DSPy ChainOfThought.</p>
<hr>
</section>
<section id="self-bootstrapping-at-inference" class="level3">
<h3 class="anchored" data-anchor-id="self-bootstrapping-at-inference">Self-Bootstrapping at inference</h3>
<p>During training, you iterate a fixed number of times and deploy the result. At inference time, you can keep iterating <em>on a single hard problem</em> until it’s solved—and GEPA’s reflective loop creates a self-bootstrapping dynamic:</p>
<ol type="1">
<li><strong>Round 1</strong>: Generate rollout → compiler error (“undefined variable x”) → reflect → propose fix</li>
<li><strong>Round 2</strong>: Code compiles → runtime error (division by zero) → reflect → propose fix<br>
</li>
<li><strong>Round 3</strong>: Runtime works → wrong output (“expected 42, got 41”) → reflect → propose fix</li>
<li><strong>Round 4</strong>: Correct output ✓</li>
</ol>
<p>Each iteration surfaces <em>the next</em> failure mode—you can’t discover the runtime error until the compile error is fixed. Traditional sampling generates 100 candidates that all hit the same compiler error. GEPA’s iterative reflection <em>progresses through</em> the failure cascade.</p>
<blockquote class="blockquote">
<p><em>“It identifies new challenges that the system is going to encounter as well as at every step it is going to propose a solution to that challenge. So it’s kind of like self-bootstrapping data to train itself.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p>This is why test-time GEPA can solve problems that stumped training: it has the budget to chase failure modes deeper than any fixed training run.</p>
<blockquote class="blockquote">
<p><strong>Note</strong>: When optimizing a batch of tasks, Pareto selection ensures that fixing one problem doesn’t discard prompts that solved others—see “Cross-Task Transfer Within a Batch” below.</p>
</blockquote>
<hr>
</section>
<section id="cross-task-transfer-within-a-batch" class="level3">
<h3 class="anchored" data-anchor-id="cross-task-transfer-within-a-batch">Cross-Task Transfer Within a Batch</h3>
<p>When solving related tasks (e.g., a batch of <a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/">CUDA kernels</a>), insights compound across the batch:</p>
<blockquote class="blockquote">
<p><em>“All of these tasks are highly related. So if I discover an insight that works well on task one, there is a high possibility that it will also work well on task two. So what happens is I use the rollout from task one to update my prompt and then I use that prompt to generate the solution for task two.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p>The frontier maintains multiple specialized prompts simultaneously:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Prompt</th>
<th>Specialization</th>
<th>Problems solved</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>P_conv</td>
<td>Convolutional operators</td>
<td>#1, #4, #7</td>
</tr>
<tr class="even">
<td>P_reduce</td>
<td>Reduction/summation operators</td>
<td>#2, #5, #8</td>
</tr>
<tr class="odd">
<td>P_matmul</td>
<td>Matrix multiplication</td>
<td>#3, #6</td>
</tr>
</tbody>
</table>
<blockquote class="blockquote">
<p><em>“One prompt will be highly specialized to the convolution one and this can work well for three-four task instances. Another might specialize to the summation one and this could cater to another three-four instances.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p>When a new problem arrives, GEPA tries candidates from across the frontier—the convolution specialist might crack it, or the reduction specialist, or insights from both might merge.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/gepa-deepdive/static/cross_task_transfer.webp" class="img-fluid figure-img"></p>
<figcaption>Cross-Task Insight Transfer</figcaption>
</figure>
</div>
<hr>
</section>
<section id="background-optimization-loops" class="level3">
<h3 class="anchored" data-anchor-id="background-optimization-loops">Background Optimization Loops</h3>
<p>The self-bootstrapping pattern suggests a natural application: <strong>background GEPA loops for personalization</strong> in tools like Cursor and other AI-assisted environments.</p>
<blockquote class="blockquote">
<p><em>“For your cursor agent use case, I can imagine that there can be a background GEPA loop that runs continuously and every time you give it feedback, it iterates and generates a new generalizing prompt… cursor can learn a user-specific prompt that works well specifically for you.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p>Every correction you provide—rejecting a verbose explanation, fixing a code style—becomes a training signal that accumulates into a personalized prompt. The infrastructure isn’t widespread yet, but the pattern points toward continuous adaptation rather than one-shot optimization.</p>
<hr>
</section>
<section id="what-gepa-stores" class="level3">
<h3 class="anchored" data-anchor-id="what-gepa-stores">What GEPA Stores</h3>
<p>For each task in the batch, GEPA tracks both artifacts:</p>
<blockquote class="blockquote">
<p><em>“GEPA tracks the best prompt per test case and you can store both the prompt as well as the output generated by that prompt.”</em> — Lakshya A Agrawal</p>
</blockquote>
<ul>
<li><strong>Best outputs</strong> — the actual solutions, ready to use</li>
<li><strong>Best prompts</strong> — specialized strategies representing different subdomains of your problem space</li>
</ul>
<p>You can deploy these domain-specific prompts for future similar tasks, or simply extract the outputs and move on.</p>
<hr>
<p>When the conditions <em>are</em> right—rich feedback, high-value tasks worth the compute, domains where the LLM has strong priors—test-time GEPA can beat sampling-based approaches. The next section demonstrates this on code generation: GEPA-optimized CUDA kernels that exceed human-written PyTorch baselines, in a domain where even frontier models like OpenAI-o1 and DeepSeek-R1 match the baseline on less than 20% of tasks.</p>
</section>
<section id="case-study-code-optimization-for-novel-hardware" class="level3">
<h3 class="anchored" data-anchor-id="case-study-code-optimization-for-novel-hardware">Case Study: Code Optimization for Novel Hardware</h3>
<p>The GEPA paper demonstrates test-time search on <strong>code optimization for hardware with limited pre-training data</strong>—a domain well-suited to GEPA’s reflective approach.</p>
<section id="amd-npu-kernels-optimization-without-pre-training-knowledge" class="level4">
<h4 class="anchored" data-anchor-id="amd-npu-kernels-optimization-without-pre-training-knowledge">AMD NPU Kernels: Optimization Without Pre-Training Knowledge</h4>
<p>AMD’s NPU (Neural Processing Unit) represents a novel hardware architecture. LLMs have minimal pre-training knowledge about its programming model, memory hierarchy, or optimization patterns. The <a href="https://arxiv.org/abs/2507.14403">NPUEval benchmark</a> shows how difficult this is: even with compiler feedback and RAG, state-of-the-art LLMs achieve only ~10% mean vectorization score.</p>
<p>GEPA’s approach doesn’t require prior examples—it iteratively generates kernels, receives compiler errors or performance metrics, reflects on feedback, and proposes targeted improvements. The compiler error messages contain the fix: “Symbol not found: <code>npu_matmul</code>” triggers reflection that surfaces the correct API.</p>
<blockquote class="blockquote">
<p><em>“GEPA can be used to generate optimized kernels for AMD’s NPU hardware, which is a very novel hardware architecture, without any pre-training knowledge because of how novel the architecture is.”</em> — Lakshya A Agrawal</p>
</blockquote>
</section>
<section id="cuda-kernels-outperforming-human-baselines" class="level4">
<h4 class="anchored" data-anchor-id="cuda-kernels-outperforming-human-baselines">CUDA Kernels: Outperforming Human Baselines</h4>
<p><a href="https://scalingintelligence.stanford.edu/blogs/kernelbench/">KernelBench</a> (Stanford, 2025) evaluates LLMs on generating efficient CUDA kernels. The benchmark sets a low baseline: <strong>frontier reasoning models match the PyTorch baseline on less than 20% of tasks</strong> using the fast₁ metric (correct <em>and</em> faster than PyTorch). Efficient GPU programming requires optimization patterns (memory coalescing, shared memory tiling, warp-level primitives) that models haven’t learned to apply reliably.</p>
<p>The KernelBench paper shows that feedback-driven refinement improves results substantially—fast₁ scores jumped 3-6x when execution results and profiler feedback were provided in context. GEPA applies this same principle systematically, with Pareto tracking to preserve diverse optimization strategies rather than ad-hoc iteration.</p>
<blockquote class="blockquote">
<p><em>“For CUDA we show results on KernelBench where GEPA was able to generate better kernels, sometimes even outperforming the PyTorch baseline which are human-written.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p>The self-bootstrapping dynamic is especially effective here: each compilation error or profiler bottleneck reveals the next fix, letting GEPA progress through failure cascades that stump one-shot sampling.</p>
</section>
<section id="cross-kernel-transfer" class="level4">
<h4 class="anchored" data-anchor-id="cross-kernel-transfer">Cross-Kernel Transfer</h4>
<p>When optimizing a batch of related kernels, insights compound across tasks:</p>
<blockquote class="blockquote">
<p><em>“If I discover an insight that works well on task one, there is a high possibility that it will also work well on task two.”</em> — Lakshya A Agrawal</p>
</blockquote>
<p>As GEPA optimizes each kernel, the Pareto frontier accumulates specialized prompts—one excelling at memory-bound operations (convolutions), another at compute-bound work (matrix multiplies), a third at reductions. Memory tiling strategies discovered on convolution kernels may transfer to pooling; warp-level primitives learned for reductions may help softmax. This cross-task transfer is why batch optimization outperforms solving each kernel independently.</p>
</section>
</section>
</section>
<section id="conclusion-when-to-reach-for-gepa" class="level2">
<h2 class="anchored" data-anchor-id="conclusion-when-to-reach-for-gepa">Conclusion: When to Reach for GEPA</h2>
<p>We opened with a familiar bind: 50 labeled examples, a prompt that works 70% of the time, and no scalable path forward. GEPA changes that calculus—matching RL’s optimization performance at a fraction of the sample cost by doing something RL can’t: <em>reading the feedback</em>.</p>
<p>On AIME math problems: 46.6% → 56.6%. On MATH benchmark: 67% → 93%. CUDA kernels that outperform human-written PyTorch baselines. All through prompt optimization alone, no fine-tuning required.</p>
<p>As LLMs have gotten better at self-reflection, this kind of optimization has become practical. Where RL needs thousands of trajectories to statistically isolate what went wrong, GEPA reads the compiler error and proposes the fix.</p>
<hr>
<section id="use-gepa-for-train-then-generalize-when" class="level3">
<h3 class="anchored" data-anchor-id="use-gepa-for-train-then-generalize-when">Use GEPA for Train-Then-Generalize When:</h3>
<ul>
<li>You have <strong>rich textual feedback</strong> (compiler errors, profiler output, LLM-as-judge rubrics, expert solutions)</li>
<li>Your evaluation budget is <strong>limited</strong> (50-500 examples, not 5,000)</li>
<li>You’re optimizing <strong>compound AI systems</strong> where prompts orchestrate multi-step pipelines</li>
<li>You need <strong>interpretable results</strong>—prompts you can read, edit, and reason about</li>
</ul>
</section>
<section id="use-gepa-for-test-time-search-when" class="level3">
<h3 class="anchored" data-anchor-id="use-gepa-for-test-time-search-when">Use GEPA for Test-Time Search When:</h3>
<ul>
<li>You have a <strong>batch of high-value tasks</strong> worth the compute investment</li>
<li>Each task produces <strong>execution feedback</strong> (tests, profilers, validators)</li>
<li>Tasks are <strong>related enough</strong> for cross-task transfer to help</li>
</ul>
</section>
<section id="stick-with-traditional-approaches-when" class="level3">
<h3 class="anchored" data-anchor-id="stick-with-traditional-approaches-when">Stick with Traditional Approaches When:</h3>
<ul>
<li>You have <strong>abundant labeled data</strong> and compute budget for fine-tuning</li>
<li>Feedback is <strong>purely scalar</strong> with no explanatory signal</li>
<li>The task is <strong>already solved</strong> by few-shot prompting</li>
<li>You need <strong>sub-second latency</strong></li>
</ul>
<hr>
</section>
<section id="get-started" class="level3">
<h3 class="anchored" data-anchor-id="get-started">Get Started</h3>
<p>Ready to try GEPA on your own pipelines?</p>
<ul>
<li><strong><a href="https://dspy.ai/tutorials/gepa_aime/">GEPA for AIME Tutorial</a></strong> — Complete walkthrough from setup to optimized results</li>
<li><strong><a href="https://dspy.ai/api/optimizers/GEPA/overview/">GEPA API Reference</a></strong> — Full parameter documentation</li>
<li><strong><a href="https://arxiv.org/abs/2507.19457">Paper</a></strong> — Algorithm details and experimental methodology</li>
</ul>
<p>The core insight is simple: your LLM pipelines already produce rich textual feedback. GEPA just reads it—and learns.</p>


</section>
</section>

 ]]></description>
  <category>code</category>
  <category>research paper</category>
  <category>analysis</category>
  <category>reimplementation</category>
  <guid>https://risheekkumar.in/posts/gepa-deepdive/gepa_final_article.html</guid>
  <pubDate>Sat, 20 Dec 2025 18:30:00 GMT</pubDate>
  <media:content url="https://risheekkumar.in/posts/gepa-deepdive/blogpost_image.webp" medium="image" type="image/webp"/>
</item>
<item>
  <title>Building a Bitcoin Blockchain from Scratch in Python</title>
  <dc:creator>Risheek kumar B</dc:creator>
  <link>https://risheekkumar.in/posts/bitcoin_from_scratch/bitcoin_from_scratch.html</link>
  <description><![CDATA[ 




<script>
document.addEventListener("DOMContentLoaded", () => {
  const article = document.querySelector("#quarto-document-content");
  const output = document.querySelector(".post-reading-time-value");

  if (!article || !output) return;

  const copy = article.cloneNode(true);
  copy.querySelector("#title-block-header")?.remove();
  copy.querySelectorAll("pre, code, script, style").forEach((element) => element.remove());

  const words = copy.textContent.trim().match(/\S+/g)?.length ?? 0;
  const minutes = Math.max(1, Math.ceil(words / 200));
  output.textContent = minutes + " min read";
});
</script>


<p>This notebook walks through the core concepts of Bitcoin by building a minimal blockchain in Python. We cover three major areas of cryptography and how they fit together:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 25%">
<col style="width: 28%">
<col style="width: 46%">
</colgroup>
<thead>
<tr class="header">
<th>Category</th>
<th>Algorithm</th>
<th>Role in Bitcoin</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Hashing</strong></td>
<td>SHA-256</td>
<td>One-way fingerprints for blocks, transactions, mining</td>
</tr>
<tr class="even">
<td><strong>Asymmetric Crypto</strong></td>
<td>ECDSA (secp256k1)</td>
<td>Public/private key pairs for identities &amp; signing</td>
</tr>
<tr class="odd">
<td><strong>Symmetric Crypto</strong></td>
<td>AES, etc.</td>
<td>Used in wallets, not in the core protocol</td>
</tr>
</tbody>
</table>
<section id="key-concepts" class="level2">
<h2 class="anchored" data-anchor-id="key-concepts">Key Concepts</h2>
<ul>
<li><strong>UTXO Model:</strong> Bitcoin doesn’t use accounts/balances. It uses Unspent Transaction Outputs (UTXOs), like individual cash bills. Your “balance” is the sum of all UTXOs your keys can unlock.</li>
<li><strong>Digital Signatures:</strong> Prove you own a UTXO without revealing your private key.</li>
<li><strong>Proof of Work:</strong> Miners brute-force a <code>nonce</code> until the block’s hash starts with enough zeros.</li>
<li><strong>Chain Linking:</strong> Each block includes the previous block’s hash, making tampering evident.</li>
</ul>
</section>
<section id="cryptographic-hashing-with-sha-256" class="level2">
<h2 class="anchored" data-anchor-id="cryptographic-hashing-with-sha-256">1. Cryptographic Hashing with SHA-256</h2>
<section id="hashing-vs.-pythons-built-in-hash" class="level3">
<h3 class="anchored" data-anchor-id="hashing-vs.-pythons-built-in-hash">Hashing vs.&nbsp;Python’s built-in <code>hash()</code></h3>
<p>Python’s <code>hash()</code> uses <strong>SipHash-1-3</strong> — a fast, non-cryptographic hash designed for dictionary lookups. It’s: - Randomized per session (to prevent HashDoS attacks) - NOT collision-resistant or suitable for security</p>
<p>Bitcoin needs <strong>SHA-256</strong> — deterministic, collision-resistant, and one-way.</p>
<p>SHA-256 isn’t used to <em>hide</em> data — it’s used as a <strong>fingerprint</strong>. Given a hash, you can never work backwards to the original input. But given an input, you’ll always get the same hash. With <img src="https://latex.codecogs.com/png.latex?2%5E%7B256%7D"> possible outputs, accidental collisions are essentially impossible.</p>
<div id="16eb03a7" class="cell" data-execution_count="1">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> hashlib</span>
<span id="cb1-2"></span>
<span id="cb1-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># SHA-256 takes bytes and returns a 64-char hex string.</span></span>
<span id="cb1-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Same input -&gt; same output, every time.</span></span>
<span id="cb1-5">hashlib.sha256(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">b'123'</span>).hexdigest()</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="1">
<pre><code>'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'</code></pre>
</div>
</div>
<div id="87d502f9" class="cell" data-execution_count="2">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># A helper: .digest() returns raw bytes (not hex), which is what secp256k1 expects.</span></span>
<span id="cb3-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> hash_fn(msg): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> hashlib.sha256(msg).digest()</span></code></pre></div></div>
</div>
</section>
</section>
<section id="elliptic-curve-cryptography-ecc" class="level2">
<h2 class="anchored" data-anchor-id="elliptic-curve-cryptography-ecc">2. Elliptic Curve Cryptography (ECC)</h2>
<section id="what-is-ecc" class="level3">
<h3 class="anchored" data-anchor-id="what-is-ecc">What is ECC?</h3>
<p>Elliptic Curve Cryptography builds on the math of elliptic curves — curves defined by:</p>
<p><img src="https://latex.codecogs.com/png.latex?y%5E2%20=%20x%5E3%20+%20ax%20+%20b"></p>
<p>Bitcoin uses the <strong>SECP256k1</strong> curve where <img src="https://latex.codecogs.com/png.latex?a=0,%20b=7">:</p>
<p><img src="https://latex.codecogs.com/png.latex?y%5E2%20=%20x%5E3%20+%207"></p>
<p>The <strong>hard problem</strong>: given a starting point <img src="https://latex.codecogs.com/png.latex?G"> (the Generator), your private key is a big random number <img src="https://latex.codecogs.com/png.latex?k">, and your public key is <img src="https://latex.codecogs.com/png.latex?k%20%5Ctimes%20G"> (point on the curve). Computing <img src="https://latex.codecogs.com/png.latex?k%20%5Ctimes%20G"> is easy, but reversing it (finding <img src="https://latex.codecogs.com/png.latex?k"> from the public key) is computationally impossible with a 256-bit key.</p>
</section>
<section id="why-not-rsa-and-why-ecc-wins" class="level3">
<h3 class="anchored" data-anchor-id="why-not-rsa-and-why-ecc-wins">Why not RSA? (And why ECC wins)</h3>
<p>The most common public-key system before ECC was <strong>RSA</strong> (Rivest–Shamir–Adleman, 1977), still widely used in TLS/HTTPS, email encryption, and digital certificates. RSA’s security relies on the <strong>integer factorization problem</strong>: given two large primes <img src="https://latex.codecogs.com/png.latex?p"> and <img src="https://latex.codecogs.com/png.latex?q">, it’s easy to compute their product <img src="https://latex.codecogs.com/png.latex?n%20=%20p%20%5Ctimes%20q">, but given <img src="https://latex.codecogs.com/png.latex?n"> alone, factoring it back into <img src="https://latex.codecogs.com/png.latex?p"> and <img src="https://latex.codecogs.com/png.latex?q"> is computationally infeasible for large keys.</p>
<ul>
<li><strong>Public key:</strong> <img src="https://latex.codecogs.com/png.latex?(n,%20e)"> — used to encrypt or verify</li>
<li><strong>Private key:</strong> <img src="https://latex.codecogs.com/png.latex?(n,%20d)"> — used to decrypt or sign</li>
</ul>
<p>RSA works, but it has a problem. Factoring is vulnerable to sub-exponential algorithms like the General Number Field Sieve — attacks smarter than brute force. To compensate, RSA key sizes must keep growing: 2048-bit is the bare minimum today, 3072+ recommended.</p>
<p>Other systems (DSA, Diffie-Hellman, ElGamal) have the same weakness — they all need large keys. This is what makes ECC special: the elliptic curve discrete logarithm problem has <strong>no known sub-exponential attack</strong>. It’s fundamentally harder per bit:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Security Level</th>
<th>RSA Key Size</th>
<th>ECC Key Size</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>128-bit</td>
<td>3072 bits</td>
<td><strong>256 bits</strong></td>
</tr>
<tr class="even">
<td>192-bit</td>
<td>7680 bits</td>
<td><strong>384 bits</strong></td>
</tr>
<tr class="odd">
<td>256-bit</td>
<td>15360 bits</td>
<td><strong>521 bits</strong></td>
</tr>
</tbody>
</table>
<p>A 256-bit ECC key provides security equivalent to a ~3072-bit RSA key — that’s over <strong>10x smaller</strong> for the same security. For Bitcoin, this matters enormously:</p>
<ul>
<li><strong>Smaller signatures &amp; keys</strong> → less data per transaction → lower fees</li>
<li><strong>Faster verification</strong> → nodes can validate blocks more quickly</li>
<li><strong>Less storage &amp; bandwidth</strong> → critical for a global decentralized network with thousands of nodes</li>
</ul>
<p>In short: RSA works, but ECC gives you the same (or better) security in a fraction of the size. For a blockchain where every byte counts, that’s a big deal.</p>
</section>
<section id="further-reading" class="level3">
<h3 class="anchored" data-anchor-id="further-reading">Further Reading</h3>
<ul>
<li><strong>ECC:</strong> <a href="https://blog.cloudflare.com/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/">Cloudflare’s Primer on ECC</a> · <a href="https://en.wikipedia.org/wiki/Elliptic-curve_cryptography">Wikipedia: Elliptic-curve cryptography</a></li>
<li><strong>RSA:</strong> <a href="https://brilliant.org/wiki/rsa-encryption/">Brilliant: RSA Encryption</a> · <a href="https://en.wikipedia.org/wiki/RSA_cryptosystem">Wikipedia: RSA cryptosystem</a> · <a href="https://www.cryptool.org/en/cto/rsa-step-by-step/">CrypTool: RSA step-by-step</a></li>
</ul>
<p>Below, we’ll first understand <em>why</em> Bitcoin uses these curves, and then visualize them — first over the real numbers (smooth), then over a finite field (discrete points).</p>
</section>
<section id="what-do-elliptic-curves-actually-achieve" class="level3">
<h3 class="anchored" data-anchor-id="what-do-elliptic-curves-actually-achieve">What Do Elliptic Curves Actually Achieve?</h3>
<p>Before we look at the curves visually, it helps to understand <em>why</em> Bitcoin uses them at all.</p>
<p><strong>The core problem:</strong> Alice wants to prove she owns some bitcoin, without revealing her secret (private key). She needs a math operation that’s:</p>
<ul>
<li><strong>Easy to do forward:</strong> take a number, multiply it by a point on the curve → get a new point</li>
<li><strong>Impossible to reverse:</strong> given that new point, figure out the original number</li>
</ul>
<p>Elliptic curves give us exactly this. Your <strong>private key</strong> is just a big random number <img src="https://latex.codecogs.com/png.latex?k">. Your <strong>public key</strong> is that number multiplied by the generator point <img src="https://latex.codecogs.com/png.latex?G"> on the curve: <img src="https://latex.codecogs.com/png.latex?K%20=%20k%20%5Ctimes%20G">. Anyone can verify your signature using the public key, but no one can work backwards to find <img src="https://latex.codecogs.com/png.latex?k">.</p>
<p><strong>Why curves specifically?</strong> Because point multiplication on an elliptic curve is the hardest one-way function we know of per bit of key size — harder than RSA’s factoring problem. Think of it like mixing paint: easy to blend two colors together, but nearly impossible to look at the result and figure out exactly which two colors were used.</p>
<p>With the <em>why</em> established, let’s talk about what to look for in the diagrams.</p>
<p><strong>Lets look at the Elliptical curves for intuition</strong> The diagrams below aren’t security proofs — they’re intuition for two things that matter later:</p>
<ol type="1">
<li><p><strong>Symmetry</strong>: for every point <img src="https://latex.codecogs.com/png.latex?(x,%20y)"> there’s a matching <img src="https://latex.codecogs.com/png.latex?(x,%20-y)">. This is why Bitcoin can compress a public key to just the <img src="https://latex.codecogs.com/png.latex?x">-coordinate plus a single parity bit — saving ~50% space on every transaction.</p></li>
<li><p><strong>Discrete points</strong>: Bitcoin doesn’t use the smooth curve — it uses a finite field (mod a prime <img src="https://latex.codecogs.com/png.latex?p">). All the point addition and multiplication still works, but on a scattered cloud of discrete points. The smooth curve is just for intuition.</p></li>
</ol>
<p>With that in mind, here’s what they look like:</p>
<div id="ac2774cc" class="cell" data-execution_count="3">
<details class="code-fold">
<summary>Code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb4-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> matplotlib.pyplot <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> plt</span>
<span id="cb4-3"></span>
<span id="cb4-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- Smooth curve (over real numbers, for intuition) ---</span></span>
<span id="cb4-5">x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linspace(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1000</span>)</span>
<span id="cb4-6">y_sq <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span></span>
<span id="cb4-7">mask <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> y_sq <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb4-8">x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> x[mask]</span>
<span id="cb4-9">y_pos <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.sqrt(x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>)</span>
<span id="cb4-10">y_neg <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>y_pos</span>
<span id="cb4-11"></span>
<span id="cb4-12">fig, (ax1, ax2) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> plt.subplots(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, figsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>))</span>
<span id="cb4-13"></span>
<span id="cb4-14">ax1.plot(x, y_pos, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'b'</span>)</span>
<span id="cb4-15">ax1.plot(x, y_neg, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'b'</span>)</span>
<span id="cb4-16">ax1.axhline(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, color<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'k'</span>, linewidth<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>)</span>
<span id="cb4-17">ax1.axvline(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, color<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'k'</span>, linewidth<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>)</span>
<span id="cb4-18">ax1.set_title(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'y² = x³ + 7 (over reals)'</span>)</span>
<span id="cb4-19">ax1.grid(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb4-20"></span>
<span id="cb4-21"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- Finite field (mod 97, for visualization) ---</span></span>
<span id="cb4-22">p <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">97</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># small prime for visualization (Bitcoin uses a 256-bit prime)</span></span>
<span id="cb4-23">points <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb4-24"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> xf <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(p):</span>
<span id="cb4-25">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> yf <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(p):</span>
<span id="cb4-26">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (yf<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>yf <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> xf<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%</span> p <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>:</span>
<span id="cb4-27">            points.append((xf, yf))</span>
<span id="cb4-28"></span>
<span id="cb4-29">xs, ys <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">zip</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>points)</span>
<span id="cb4-30">ax2.scatter(xs, ys, s<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, color<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'blue'</span>)</span>
<span id="cb4-31">ax2.set_title(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'y² = x³ + 7 (mod </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>p<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">) — </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(points)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> points'</span>)</span>
<span id="cb4-32">ax2.grid(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb4-33"></span>
<span id="cb4-34">plt.tight_layout()</span>
<span id="cb4-35">plt.show()</span>
<span id="cb4-36"></span>
<span id="cb4-37"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Notice the symmetry: for every (x, y), the point (x, -y) also exists."</span>)</span>
<span id="cb4-38"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"This is why we only need to store x + a parity bit to compress a public key."</span>)</span></code></pre></div></div>
</details>
<div class="cell-output cell-output-display">
<div>
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/bitcoin_from_scratch/bitcoin_from_scratch_files/figure-html/cell-4-output-1.png" class="img-fluid figure-img"></p>
</figure>
</div>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>Notice the symmetry: for every (x, y), the point (x, -y) also exists.
This is why we only need to store x + a parity bit to compress a public key.</code></pre>
</div>
</div>
</section>
<section id="public-key-compression" class="level3">
<h3 class="anchored" data-anchor-id="public-key-compression">Public Key Compression</h3>
<p>Since <img src="https://latex.codecogs.com/png.latex?y%5E2%20=%20x%5E3%20+%207">, given <img src="https://latex.codecogs.com/png.latex?x"> there are only two possible <img src="https://latex.codecogs.com/png.latex?y"> values (one even, one odd). So a public key can be compressed from 65 bytes (full <img src="https://latex.codecogs.com/png.latex?x,%20y">) down to <strong>33 bytes</strong>: just the full <img src="https://latex.codecogs.com/png.latex?x"> coordinate + a prefix byte (<code>02</code> = even <img src="https://latex.codecogs.com/png.latex?y">, <code>03</code> = odd <img src="https://latex.codecogs.com/png.latex?y">).</p>
</section>
<section id="address-generation-pipeline" class="level3">
<h3 class="anchored" data-anchor-id="address-generation-pipeline">Address Generation Pipeline</h3>
<pre><code>private key (32 bytes)
    ↓  ECC multiplication
compressed public key (33 bytes)
    ↓  SHA256 → RIPEMD160
address hash (20 bytes)
    ↓  Base58Check encoding
Bitcoin address (e.g. 1A1zP1...)</code></pre>
</section>
</section>
<section id="key-generation" class="level2">
<h2 class="anchored" data-anchor-id="key-generation">3. Key Generation</h2>
<p>We use the <code>secp256k1</code> library to generate a real key pair. The private key is 32 random bytes; the public key is the point <img src="https://latex.codecogs.com/png.latex?k%20%5Ctimes%20G"> on the curve.</p>
<div id="e0ec211c" class="cell" data-execution_count="4">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> os</span>
<span id="cb7-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> secp256k1</span>
<span id="cb7-3"></span>
<span id="cb7-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> get_account():</span>
<span id="cb7-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Generate a random private key and derive the public key.</span></span>
<span id="cb7-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    Returns (PrivateKey, PublicKey) objects from the secp256k1 library."""</span></span>
<span id="cb7-7">    private_key <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> secp256k1.PrivateKey(os.urandom(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">32</span>))</span>
<span id="cb7-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> private_key, private_key.pubkey</span></code></pre></div></div>
</div>
</section>
<section id="digital-signatures-signing-and-verifying" class="level2">
<h2 class="anchored" data-anchor-id="digital-signatures-signing-and-verifying">4. Digital Signatures — Signing and Verifying</h2>
<p>A digital signature proves ownership of a private key <em>without revealing it</em>.</p>
<section id="how-it-works" class="level3">
<h3 class="anchored" data-anchor-id="how-it-works">How it works</h3>
<ol type="1">
<li><strong>Signing (sender):</strong> Hash the message, then combine the hash + private key using ECDSA to produce a signature.</li>
<li><strong>Verifying (miner/anyone):</strong> Feed the message hash + signature + public key into the verification algorithm. Returns <code>True</code> if valid.</li>
</ol>
</section>
<section id="properties" class="level3">
<h3 class="anchored" data-anchor-id="properties">Properties</h3>
<ul>
<li><strong>Proof of Ownership:</strong> Only the private key holder could have produced the signature.</li>
<li><strong>Tamper-Proof:</strong> Changing even one character in the message invalidates the signature.</li>
</ul>
</section>
<section id="note-on-the-math" class="level3">
<h3 class="anchored" data-anchor-id="note-on-the-math">Note on the math</h3>
<p>We don’t just hash(private_key + message). That would require the verifier to know the private key. Instead, ECDSA creates a mathematical puzzle that can be <em>verified</em> with just the public key, but only <em>solved</em> with the private key.</p>
<div id="17acdca7" class="cell" data-execution_count="5">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Step 1: Create and hash a message</span></span>
<span id="cb8-2">msg <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">b'Send 1 satoshi to Bob'</span></span>
<span id="cb8-3">msg_hash <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> hash_fn(msg)</span>
<span id="cb8-4"></span>
<span id="cb8-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Step 2: Generate a key pair</span></span>
<span id="cb8-6">priv_key, pub_key <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> get_account()</span>
<span id="cb8-7"></span>
<span id="cb8-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Step 3: Sign the hash with the private key</span></span>
<span id="cb8-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># The signature is 64 bytes, mathematically tied to both the message and private key.</span></span>
<span id="cb8-10">signature <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> priv_key.ecdsa_sign(msg_hash)</span>
<span id="cb8-11"></span>
<span id="cb8-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Step 4: Verify using the public key (anyone can do this)</span></span>
<span id="cb8-13">is_valid <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pub_key.ecdsa_verify(msg_hash, signature)</span>
<span id="cb8-14"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Signature valid: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>is_valid<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb8-15"></span>
<span id="cb8-16"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Step 5: Tamper with the message — signature should fail</span></span>
<span id="cb8-17">tampered_msg <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">b'Send 10 satoshi to Bob'</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1 satoshi -&gt; 10 satoshis</span></span>
<span id="cb8-18">tampered_hash <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> hash_fn(tampered_msg)</span>
<span id="cb8-19">is_valid_tampered <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pub_key.ecdsa_verify(tampered_hash, signature)</span>
<span id="cb8-20"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Tampered signature valid: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>is_valid_tampered<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>Signature valid: True
Tampered signature valid: False</code></pre>
</div>
</div>
</section>
</section>
<section id="the-utxo-model-and-transactions" class="level2">
<h2 class="anchored" data-anchor-id="the-utxo-model-and-transactions">5. The UTXO Model and Transactions</h2>
<section id="utxo-unspent-transaction-output" class="level3">
<h3 class="anchored" data-anchor-id="utxo-unspent-transaction-output">UTXO = Unspent Transaction Output</h3>
<p>Bitcoin doesn’t track “accounts” with balances. Instead, it tracks individual UTXOs — like cash bills. If you have a $20 bill and buy a $5 coffee: - Your $20 UTXO is <strong>consumed</strong> as an input - Two new UTXOs are <strong>created</strong> as outputs: $5 to the shop, $15 change back to you</p>
<p>Your “balance” = sum of all UTXOs your private key can unlock.</p>
</section>
<section id="spending-multiple-utxos" class="level3">
<h3 class="anchored" data-anchor-id="spending-multiple-utxos">Spending multiple UTXOs</h3>
<p>A transaction can have <strong>multiple inputs</strong> — just like pulling three $5 bills from your wallet to pay for a $12 item. Each input needs its own signature.</p>
<div id="848050c8" class="cell" data-execution_count="6">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb10-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> fastcore.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">all</span> <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span></span>
<span id="cb10-2"></span>
<span id="cb10-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> UTXO:</span>
<span id="cb10-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""An Unspent Transaction Output.</span></span>
<span id="cb10-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - amount:   value in satoshis</span></span>
<span id="cb10-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - pub_key:  the secp256k1.PublicKey that can spend this UTXO</span></span>
<span id="cb10-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - spent:    whether this UTXO has been consumed by a transaction</span></span>
<span id="cb10-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    """</span></span>
<span id="cb10-9">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, amount, pub_key, spent<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>):</span>
<span id="cb10-10">        store_attr()</span></code></pre></div></div>
</div>
<div id="8c96def2" class="cell" data-execution_count="7">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb11-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> Transaction:</span>
<span id="cb11-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""A Bitcoin-style transaction.</span></span>
<span id="cb11-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - inputs:   list of UTXOs being consumed (must be unspent)</span></span>
<span id="cb11-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - outputs:  list of new UTXOs being created</span></span>
<span id="cb11-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - signatures: list of ECDSA signatures, one per input</span></span>
<span id="cb11-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    """</span></span>
<span id="cb11-7">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, inputs, outputs):</span>
<span id="cb11-8">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.inputs  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> inputs   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># UTXOs we are spending</span></span>
<span id="cb11-9">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.outputs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> outputs  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># New UTXOs being created</span></span>
<span id="cb11-10"></span>
<span id="cb11-11">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> validate_input(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb11-12">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Check that total inputs &gt;= total outputs (you can't spend more than you have)."""</span></span>
<span id="cb11-13">        input_amount  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(o.amount <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.inputs)</span>
<span id="cb11-14">        output_amount <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(o.amount <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.outputs)</span>
<span id="cb11-15">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> input_amount <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> output_amount: <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb11-16">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span></span>
<span id="cb11-17"></span>
<span id="cb11-18">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> transact(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb11-19">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Process the transaction: validate math and mark inputs as spent."""</span></span>
<span id="cb11-20">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.validate_input(): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'Invalid transaction'</span></span>
<span id="cb11-21">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.inputs: o.spent <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb11-22"></span>
<span id="cb11-23">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> get_hash(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, input_index):</span>
<span id="cb11-24">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Hash the transaction data for a specific input.</span></span>
<span id="cb11-25"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">        The input_index ensures each input signs a slightly different hash,</span></span>
<span id="cb11-26"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">        preventing replay attacks (same concept as real Bitcoin)."""</span></span>
<span id="cb11-27">        tx_data <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"In:</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>[o.amount <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.inputs]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> Out:</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>[o.amount <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.outputs]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">; this is </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>input_index<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb11-28">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> hash_fn(tx_data.encode())</span>
<span id="cb11-29"></span>
<span id="cb11-30">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> sign_input(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, input_index, priv_key):</span>
<span id="cb11-31">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Sign a specific input with the given private key.</span></span>
<span id="cb11-32"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">        Each input gets its own signature — the wallet does this automatically."""</span></span>
<span id="cb11-33">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">hasattr</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'signatures'</span>):</span>
<span id="cb11-34">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.signatures <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.inputs)</span>
<span id="cb11-35">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.signatures[input_index] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> priv_key.ecdsa_sign(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.get_hash(input_index))</span>
<span id="cb11-36"></span>
<span id="cb11-37">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> verify(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb11-38">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Verify all signatures. Each input's signature must be valid against</span></span>
<span id="cb11-39"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">        the corresponding UTXO's public key."""</span></span>
<span id="cb11-40">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">all</span>(</span>
<span id="cb11-41">            inp.pub_key.ecdsa_verify(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.get_hash(idx), sig)</span>
<span id="cb11-42">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> idx, (inp, sig) <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">enumerate</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">zip</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.inputs, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.signatures))</span>
<span id="cb11-43">        )</span></code></pre></div></div>
</div>
</section>
<section id="putting-it-together-alice-sends-bitcoin-to-bob" class="level3">
<h3 class="anchored" data-anchor-id="putting-it-together-alice-sends-bitcoin-to-bob">Putting it together: Alice sends Bitcoin to Bob</h3>
<p>Alice has two UTXOs (50 + 30 = 80 satoshis). She sends 70 to Bob and keeps 10 as change. She signs each input, then we verify the whole transaction is authentic.</p>
<div id="5b364d68" class="cell" data-execution_count="8">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb12-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Generate keys for Alice and Bob</span></span>
<span id="cb12-2">alice_priv, alice_pub <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> get_account()</span>
<span id="cb12-3">bob_priv,   bob_pub   <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> get_account()</span>
<span id="cb12-4"></span>
<span id="cb12-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Create Alice's UTXOs (she owns these from previous transactions)</span></span>
<span id="cb12-6">utxo1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> UTXO(amount<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">50</span>, pub_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>alice_pub)</span>
<span id="cb12-7">utxo2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> UTXO(amount<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">30</span>, pub_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>alice_pub)</span>
<span id="cb12-8"></span>
<span id="cb12-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Build the transaction:</span></span>
<span id="cb12-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    Inputs:  50 + 30 = 80 satoshis from Alice</span></span>
<span id="cb12-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#    Outputs: 70 to Bob, 10 change to Alice</span></span>
<span id="cb12-12">tx <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Transaction(</span>
<span id="cb12-13">    inputs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[utxo1, utxo2],</span>
<span id="cb12-14">    outputs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[UTXO(amount<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">70</span>, pub_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>bob_pub), UTXO(amount<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>, pub_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>alice_pub)]</span>
<span id="cb12-15">)</span>
<span id="cb12-16"></span>
<span id="cb12-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 4. Alice signs each input (wallet does this automatically in practice)</span></span>
<span id="cb12-18">tx.sign_input(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, alice_priv)</span>
<span id="cb12-19">tx.sign_input(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, alice_priv)</span>
<span id="cb12-20"></span>
<span id="cb12-21"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 5. Verify — miners do this before including the transaction in a block</span></span>
<span id="cb12-22"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Transaction valid: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>tx<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>verify()<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb12-23"></span>
<span id="cb12-24"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 6. Process the transaction — mark old UTXOs as spent</span></span>
<span id="cb12-25">tx.transact()</span>
<span id="cb12-26"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Inputs spent:     </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>[u.spent <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> u <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> tx.inputs]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>Transaction valid: True
Inputs spent:     [True, True]</code></pre>
</div>
</div>
</section>
</section>
<section id="blocks-and-proof-of-work-mining" class="level2">
<h2 class="anchored" data-anchor-id="blocks-and-proof-of-work-mining">6. Blocks and Proof-of-Work Mining</h2>
<section id="what-is-a-block" class="level3">
<h3 class="anchored" data-anchor-id="what-is-a-block">What is a Block?</h3>
<p>A block is a container for transactions, plus metadata that links it to the chain: - <strong>transactions</strong>: list of confirmed transactions - <strong>prev_block_hash</strong>: hash of the previous block (creates the “chain”) - <strong>nonce</strong>: the number miners brute-force to find a valid hash</p>
</section>
<section id="how-mining-works" class="level3">
<h3 class="anchored" data-anchor-id="how-mining-works">How Mining Works</h3>
<p>Miners repeatedly hash the block while incrementing the <code>nonce</code>, looking for a hash that starts with a target number of zeros. This is <strong>Proof of Work</strong>: - <strong>Hard to find</strong>: millions of guesses required - <strong>Easy to verify</strong>: anyone can hash once and check - <strong>Difficulty</strong> = number of leading zeros required</p>
<p>The first miner to find a valid nonce broadcasts the block. The network verifies it, and the block is added to the chain. The miner earns a block reward + transaction fees.</p>
<div id="9c65b56d" class="cell" data-execution_count="9">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb14-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> Block:</span>
<span id="cb14-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""A single block in the blockchain.</span></span>
<span id="cb14-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - transactions:      list of Transaction objects in this block</span></span>
<span id="cb14-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - prev_block_hash:   hex string linking to the previous block</span></span>
<span id="cb14-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    - nonce:             number used once — miners increment this to find a valid hash</span></span>
<span id="cb14-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">    """</span></span>
<span id="cb14-7">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, transactions, prev_block_hash, nonce<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>):</span>
<span id="cb14-8">        store_attr()</span>
<span id="cb14-9"></span>
<span id="cb14-10">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> create_hash(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb14-11">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Hash the block contents: previous hash + transaction data + nonce."""</span></span>
<span id="cb14-12">        trans_str <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prev_block_hash</span>
<span id="cb14-13">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.transactions: trans_str <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>(o)</span>
<span id="cb14-14">        trans_str <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.nonce)</span>
<span id="cb14-15">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> hash_fn(trans_str.encode())</span>
<span id="cb14-16"></span>
<span id="cb14-17">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> mine(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, difficulty: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>):</span>
<span id="cb14-18">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Brute-force the nonce until create_hash() starts with `difficulty` zeros.</span></span>
<span id="cb14-19"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">        difficulty=4 means we need a hash starting with '0000'."""</span></span>
<span id="cb14-20">        target <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'0'</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> difficulty</span>
<span id="cb14-21">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.create_hash().<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">hex</span>().startswith(target):</span>
<span id="cb14-22">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.nonce <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb14-23">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Mined block! Nonce: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>nonce<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb14-24">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Hash:   </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>create_hash()<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">hex</span>()<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
</div>
</section>
<section id="the-genesis-block" class="level3">
<h3 class="anchored" data-anchor-id="the-genesis-block">The Genesis Block</h3>
<p>The very first block in a blockchain. Its <code>prev_block_hash</code> is all zeros because there’s nothing before it. (Bitcoin’s real genesis block was mined by Satoshi Nakamoto on January 3, 2009.)</p>
<div id="1bb16945" class="cell" data-execution_count="10">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb15-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># The genesis block contains our Alice-&gt;Bob transaction.</span></span>
<span id="cb15-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Its prev_block_hash is all zeros (nothing came before it).</span></span>
<span id="cb15-3">genesis_block <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Block(</span>
<span id="cb15-4">    transactions<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[tx],</span>
<span id="cb15-5">    prev_block_hash<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">64</span></span>
<span id="cb15-6">)</span>
<span id="cb15-7"></span>
<span id="cb15-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Mine it! Higher difficulty = more leading zeros = much longer to compute.</span></span>
<span id="cb15-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Try difficulty=2 first, then bump to 4 or 5 to see the difference.</span></span>
<span id="cb15-10">genesis_block.mine(difficulty<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>Mined block! Nonce: 60699
Hash:   0000599ce1c15af69c019c9b8563475beaeef688a6ced5923fa728edb833a551</code></pre>
</div>
</div>
</section>
</section>
<section id="the-blockchain" class="level2">
<h2 class="anchored" data-anchor-id="the-blockchain">7. The Blockchain</h2>
<p>A <code>Blockchain</code> stores the chain and enforces linking: each new block must point to the current chain tip via its <code>prev_block_hash</code>.</p>
<section id="fork-resolution" class="level3">
<h3 class="anchored" data-anchor-id="fork-resolution">Fork Resolution</h3>
<p>When two miners find a valid block at almost the same time, the chain temporarily forks. The network resolves this by always following the <strong>longest chain</strong> (most cumulative work). The tie is broken when the next block is mined on one branch, and the other branch becomes an “orphan” that’s abandoned.</p>
<div id="9fd94bb3" class="cell" data-execution_count="11">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb17-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> Blockchain:</span>
<span id="cb17-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""A simple blockchain that stores blocks and enforces hash linking."""</span></span>
<span id="cb17-3">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb17-4">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.chain <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb17-5">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prev_block_hash <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">64</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># genesis prev hash</span></span>
<span id="cb17-6"></span>
<span id="cb17-7">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> add_block(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, block):</span>
<span id="cb17-8">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Add a block only if its prev_block_hash matches the current chain tip."""</span></span>
<span id="cb17-9">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> block.prev_block_hash <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prev_block_hash: <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span></span>
<span id="cb17-10">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.chain.append(block)</span>
<span id="cb17-11">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.prev_block_hash <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.chain[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>].create_hash().<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">hex</span>()</span></code></pre></div></div>
</div>
<div id="34472a04" class="cell" data-execution_count="12">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb18-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Initialize the blockchain</span></span>
<span id="cb18-2">my_chain <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Blockchain()</span>
<span id="cb18-3"></span>
<span id="cb18-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Add the genesis block (already mined above)</span></span>
<span id="cb18-5">my_chain.add_block(genesis_block)</span>
<span id="cb18-6"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Chain length after genesis: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(my_chain.chain)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb18-7"></span>
<span id="cb18-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Mine and add a second block</span></span>
<span id="cb18-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Note: it links to the genesis block via my_chain.prev_block_hash</span></span>
<span id="cb18-10">block_2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Block(</span>
<span id="cb18-11">    transactions<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[],  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># empty for simplicity</span></span>
<span id="cb18-12">    prev_block_hash<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>my_chain.prev_block_hash</span>
<span id="cb18-13">)</span>
<span id="cb18-14"></span>
<span id="cb18-15"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Mining block 2..."</span>)</span>
<span id="cb18-16">block_2.mine(difficulty<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)</span>
<span id="cb18-17"></span>
<span id="cb18-18">my_chain.add_block(block_2)</span>
<span id="cb18-19"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">Chain length after block 2: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(my_chain.chain)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>Chain length after genesis: 1

Mining block 2...
Mined block! Nonce: 25841
Hash:   0000cc9875df0e8b172014556622fcf8a37289eafb7a5afe518c587c955852d2

Chain length after block 2: 2</code></pre>
</div>
</div>
</section>
</section>
<section id="summary-the-logical-flow-of-a-bitcoin-transaction" class="level2">
<h2 class="anchored" data-anchor-id="summary-the-logical-flow-of-a-bitcoin-transaction">Summary: The Logical Flow of a Bitcoin Transaction</h2>
<pre><code>1. IDENTITIES     Alice &amp; Bob generate private/public key pairs (secp256k1)
       ↓
2. TRANSACTION    Alice takes her UTXOs, signs them with her private key,
                  creates new UTXOs locked to Bob's public key
       ↓
3. BROADCAST      Transaction is sent to the network (mempool)
       ↓
4. BLOCK          Miner packages transactions into a block, verifies signatures
       ↓
5. MINING         Miner brute-forces the nonce until SHA-256 hash starts
                  with enough zeros (Proof of Work)
       ↓
6. CHAIN          Block is added. Old UTXOs are spent, new UTXOs are live.
                  If two miners find a block simultaneously, the longest
                  chain wins (fork resolution).</code></pre>
<section id="what-we-built" class="level3">
<h3 class="anchored" data-anchor-id="what-we-built">What we built</h3>
<table class="caption-top table">
<colgroup>
<col style="width: 27%">
<col style="width: 37%">
<col style="width: 35%">
</colgroup>
<thead>
<tr class="header">
<th>Component</th>
<th>Class/Function</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Hashing</td>
<td><code>hash_fn()</code></td>
<td>SHA-256 hash returning raw bytes</td>
</tr>
<tr class="even">
<td>Keys</td>
<td><code>get_account()</code></td>
<td>Generate secp256k1 private/public key pair</td>
</tr>
<tr class="odd">
<td>UTXO</td>
<td><code>UTXO</code></td>
<td>Unspent Transaction Output with amount, pub_key, spent status</td>
</tr>
<tr class="even">
<td>Transaction</td>
<td><code>Transaction</code></td>
<td>Groups inputs/outputs, handles signing &amp; verification</td>
</tr>
<tr class="odd">
<td>Block</td>
<td><code>Block</code></td>
<td>Holds transactions, links to previous block, mines with Proof of Work</td>
</tr>
<tr class="even">
<td>Blockchain</td>
<td><code>Blockchain</code></td>
<td>Stores the chain, enforces hash linking between blocks</td>
</tr>
</tbody>
</table>
</section>
</section>
<section id="what-else-could-we-build" class="level2">
<h2 class="anchored" data-anchor-id="what-else-could-we-build">8. What Else Could We Build?</h2>
<p>This is just the foundation. Here are natural next steps, roughly in order of complexity:</p>
<section id="near-term-extensions" class="level3">
<h3 class="anchored" data-anchor-id="near-term-extensions">Near-term extensions</h3>
<ul>
<li><strong>Merkle Trees</strong> — Instead of hashing transactions individually, Bitcoin hashes them into a binary tree. This lets you prove a transaction is in a block without downloading the whole block (SPV / “light clients”).</li>
<li><strong>Transaction Fees</strong> — The difference between inputs and outputs isn’t wasted — it goes to the miner. We already allow <code>input &gt; output</code>; we’d just need to track and award that difference.</li>
<li><strong>Coinbase Transactions</strong> — Each block contains a special first transaction that creates new coins out of nowhere (the block reward). This is how new Bitcoin enters circulation.</li>
<li><strong>Difficulty Adjustment</strong> — Real Bitcoin adjusts difficulty every 2016 blocks (~2 weeks) to keep block time at ~10 minutes, regardless of total network hashpower.</li>
</ul>
</section>
<section id="medium-term-extensions" class="level3">
<h3 class="anchored" data-anchor-id="medium-term-extensions">Medium-term extensions</h3>
<ul>
<li><strong>Networking / P2P</strong> — Nodes that discover peers, gossip transactions, and propagate blocks. Even a simple simulated network would illustrate consensus.</li>
<li><strong>Wallet with Address Derivation</strong> — Hash the public key through SHA-256 → RIPEMD-160 → Base58Check to produce real-looking Bitcoin addresses.</li>
<li><strong>Script / Locking Conditions</strong> — Bitcoin transactions use a small stack-based scripting language. The simplest case is “must provide a signature matching this public key,” but it supports multisig, timelocks, and more.</li>
<li><strong>Mempool &amp; Block Templates</strong> — A pool of unconfirmed transactions that miners select from, prioritizing higher fees.</li>
</ul>
</section>
<section id="advanced" class="level3">
<h3 class="anchored" data-anchor-id="advanced">Advanced</h3>
<ul>
<li><strong>SegWit / Witness Data</strong> — Separating signatures from the transaction data that gets hashed, fixing transaction malleability.</li>
<li><strong>SPV Verification</strong> — A lightweight client that verifies transactions using Merkle proofs without downloading the full blockchain.</li>
<li><strong>Simple Payment Verification vs.&nbsp;Full Nodes</strong> — Tradeoffs between trust, storage, and verification.</li>
<li><strong>Double-spend detection</strong> — Rejected conflicting transactions that try to spend the same UTXO twice.</li>
</ul>


</section>
</section>

 ]]></description>
  <category>code</category>
  <category>research paper</category>
  <category>impact</category>
  <guid>https://risheekkumar.in/posts/bitcoin_from_scratch/bitcoin_from_scratch.html</guid>
  <pubDate>Sat, 27 Sep 2025 18:30:00 GMT</pubDate>
  <media:content url="https://risheekkumar.in/posts/bitcoin_from_scratch/Bitcoin.webp" medium="image" type="image/webp"/>
</item>
<item>
  <title>HNSW: Hierarchical Navigable Small World Graphs for Efficient Similarity Search</title>
  <dc:creator>Risheek kumar B</dc:creator>
  <link>https://risheekkumar.in/posts/HNSW/HNSW.html</link>
  <description><![CDATA[ 




<script>
document.addEventListener("DOMContentLoaded", () => {
  const article = document.querySelector("#quarto-document-content");
  const output = document.querySelector(".post-reading-time-value");

  if (!article || !output) return;

  const copy = article.cloneNode(true);
  copy.querySelector("#title-block-header")?.remove();
  copy.querySelectorAll("pre, code, script, style").forEach((element) => element.remove());

  const words = copy.textContent.trim().match(/\S+/g)?.length ?? 0;
  const minutes = Math.max(1, Math.ceil(words / 200));
  output.textContent = minutes + " min read";
});
</script>


<p>You’re building a feature that lets users upload an image and find visually similar ones from a database of millions. The math is straightforward — compute how “close” two image embeddings are using cosine distance, return the closest matches.</p>
<p>You try the naive approach: compare the query against every single image. It works perfectly on your test set of 1,000 images. Then you deploy to production with 10 million images, and each query takes 3 seconds. Users leave.</p>
<p>This is the <strong>nearest neighbor problem</strong> — and it shows up everywhere:</p>
<ul>
<li><strong>Search engines</strong>: “find documents similar to this one”</li>
<li><strong>Recommendation systems</strong>: “users who liked this also liked…”</li>
<li><strong>RAG pipelines</strong>: “retrieve the most relevant chunks for this question”</li>
</ul>
<p>Brute force is O(n·d) per query — linear in the number of vectors. For small datasets it’s fine. For millions of vectors at production latency requirements, it’s a non-starter.</p>
<p>HNSW (Hierarchical Navigable Small World) solves this by organizing your vectors into a <strong>layered graph</strong> that you can navigate greedily — like taking highways to get close to your destination, then switching to local roads for the final stretch. Instead of checking every vector, you hop through the graph following the most promising connections.</p>
<p>The result: queries that are nearly as accurate as brute force (which is still the gold standard for exact nearest neighbors), but orders of magnitude faster. HNSW trades a tiny bit of accuracy for huge speed gains — a tradeoff that’s almost always worth it in production.</p>
<blockquote class="blockquote">
<p><em>This article is one of our learning attempts — we built a toy HNSW implementation from scratch to understand the algorithm before using production libraries. The code is simplified for clarity, not optimized for performance.</em></p>
</blockquote>
<p>Next, let’s understand the layered graph structure that makes this possible →</p>
<section id="step-1-the-data-structure" class="level2">
<h2 class="anchored" data-anchor-id="step-1-the-data-structure">Step 1: The Data Structure</h2>
<p>Before writing any algorithms, we need to decide how to <em>hold</em> all this information. HNSW organizes your data into <strong>multiple layers</strong> of a graph — each layer contains nodes (your vectors) connected to their nearest neighbors:</p>
<ul>
<li><strong>Layer 0 (bottom)</strong>: Contains <em>every</em> vector, with dense local connections. This is where precise, final search happens.</li>
<li><strong>Layer 1</strong>: A subset of vectors — roughly 30% reach here.</li>
<li><strong>Layer 2+</strong>: Exponentially sparser. Only a few “hub” vectors live up here.</li>
</ul>
<p>The key insight: each higher layer acts like an <strong>express lane</strong>. You start searching at the top layer (few nodes, fast traversal), navigate to the approximate region of your query, then drop down to the next layer to refine. Repeat until you reach layer 0 for the final, precise results.</p>
<div id="79e535f7" class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> collections <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> defaultdict</span>
<span id="cb1-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> HNSW:</span>
<span id="cb1-3">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, max_neighbors, ef_construction, max_layers):</span>
<span id="cb1-4">        store_attr()</span>
<span id="cb1-5">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.values <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {}</span>
<span id="cb1-6">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.curr_id<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb1-7">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {o:defaultdict(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(max_layers)}</span>
<span id="cb1-8">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.entry_point <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span></span>
<span id="cb1-9">    </span>
<span id="cb1-10">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__repr__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span></span>
<span id="cb1-11"></span>
<span id="cb1-12">ex <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> HNSW(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb1-13">ex</span></code></pre></div></div>
</details>
<div class="cell-output cell-output-display">
<pre><code>[0, 1, 2, 3, 4]</code></pre>
</div>
</div>
<p>With that in mind, we define the class with <code>max_neighbors</code>, <code>ef_construction</code>, and <code>max_layers</code> parameters, storing values (vectors), per-layer adjacency lists, and a global entry point.</p>
<blockquote class="blockquote">
<p><code>store_attr()</code> (from <a href="https://fastcore.fast.ai">fastcore</a>) is a convenience that auto-assigns constructor arguments to <code>self</code> — here it’s equivalent to writing <code>self.max_neighbors = max_neighbors</code>, etc.</p>
</blockquote>
<p><code>self.layers</code> is a <strong>dict of dicts</strong>. Each layer maps <code>node_id → list of neighbor node_ids</code>:</p>
<ul>
<li><code>layers[0][5] = [3, 7, 12]</code> means “in layer 0, node 5 is connected to nodes 3, 7, and 12”</li>
<li>A node appears in <code>layers[l]</code> only if its assigned max layer is <code>&gt;= l</code></li>
</ul>
<p>The <code>self.values</code> dict stores the raw vectors separately — keeping them decoupled from the graph structure makes distance calculations cleaner.</p>
<p>With the data structure in place, let’s see how vectors get inserted →</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/HNSW/static/layered_graph.png" class="img-fluid figure-img"></p>
<figcaption>HNSW Layered Graph Structure</figcaption>
</figure>
</div>
<div id="bef26046" class="cell" data-execution_count="3">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> math, random</span>
<span id="cb3-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> collections <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Counter</span>
<span id="cb3-3"></span>
<span id="cb3-4"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb3-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _get_insert_layer(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>: HNSW):</span>
<span id="cb3-6">    m_L <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> math.log(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.max_neighbors)</span>
<span id="cb3-7">    l <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>math.log(random.uniform(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m_L)</span>
<span id="cb3-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">min</span>(l, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.max_layers <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb3-9">Counter([ex._get_insert_layer() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> _ <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>)])</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="3">
<pre><code>Counter({0: 67, 1: 24, 2: 7, 4: 2})</code></pre>
</div>
</div>
<div id="ab308485" class="cell" data-execution_count="4">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb5-2"></span>
<span id="cb5-3"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb5-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _cosine_distance(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:HNSW, a, b):</span>
<span id="cb5-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Cosine distance measures the *angle* between two vectors, not their magnitude.</span></span>
<span id="cb5-6">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># We use it (instead of Euclidean) because direction matters more than length</span></span>
<span id="cb5-7">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># for high-dimensional embeddings — two vectors pointing the same way are "similar"</span></span>
<span id="cb5-8">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># even if one is much longer.</span></span>
<span id="cb5-9">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span></span>
<span id="cb5-10">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Range: 0 = identical direction, 1 = perpendicular, 2 = opposite</span></span>
<span id="cb5-11">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> np.linalg.norm(a) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">or</span> np.linalg.norm(b) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>: <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb5-12">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># cos(θ) = dot(a,b) / (|a| * |b|), then distance = 1 - cos(θ)</span></span>
<span id="cb5-13">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> np.dot(a,b) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (np.linalg.norm(a) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np.linalg.norm(b))</span>
<span id="cb5-14">k,l <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.random.randn(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>)</span>
<span id="cb5-15"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">round</span>(ex._cosine_distance(k,l).item(), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="4">
<pre><code>0.58</code></pre>
</div>
</div>
<div id="07d56162" class="cell" data-execution_count="5">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb7-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _calc_layer(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>: HNSW, item_id, layer):</span>
<span id="cb7-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Brute-force scan of all nodes in this layer — simple but O(n).</span></span>
<span id="cb7-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Production HNSW uses a greedy graph search here instead.</span></span>
<span id="cb7-5">    nodes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [(node_id, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.values[node_id]) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> node_id <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers[layer] <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> node_id <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> item_id]</span>
<span id="cb7-6">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Compute distance from our item to every other node, then sort closest-first</span></span>
<span id="cb7-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sorted</span>(</span>
<span id="cb7-8">        [(node_id, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._cosine_distance(node, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.values[item_id])) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> node_id, node <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> nodes],</span>
<span id="cb7-9">        key <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> x: x[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]</span>
<span id="cb7-10">    )</span></code></pre></div></div>
</div>
<div id="4512b946" class="cell" data-execution_count="6">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb8-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _register_item(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>: HNSW, item):</span>
<span id="cb8-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Assign an ID, store the vector, and pick a random max layer</span></span>
<span id="cb8-4">    item_id <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.curr_id</span>
<span id="cb8-5">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.values[item_id] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> item</span>
<span id="cb8-6">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.curr_id <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb8-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> item_id, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._get_insert_layer()</span>
<span id="cb8-8"></span>
<span id="cb8-9"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb8-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _trim_neighbors(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>: HNSW, node_id, layer):</span>
<span id="cb8-11">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># If a node has too many neighbors, keep only the closest M</span></span>
<span id="cb8-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers[layer][node_id]) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.max_neighbors:</span>
<span id="cb8-13">        neighbors <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._calc_layer(node_id, layer)</span>
<span id="cb8-14">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers[layer][node_id] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [x <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> x, _ <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> neighbors[:<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.max_neighbors]]</span>
<span id="cb8-15"></span>
<span id="cb8-16"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb8-17"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _connect_node(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>: HNSW, item_id, layer):</span>
<span id="cb8-18">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Find M nearest neighbors in this layer and create bidirectional edges</span></span>
<span id="cb8-19">    nearest <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._calc_layer(item_id, layer)[:<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.max_neighbors]</span>
<span id="cb8-20">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers[layer][item_id] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [n_id <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n_id, _ <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> nearest]</span>
<span id="cb8-21">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n_id, _ <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> nearest:</span>
<span id="cb8-22">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers[layer][n_id].append(item_id)       <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># reverse edge</span></span>
<span id="cb8-23">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._trim_neighbors(n_id, layer)              <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># keep degree ≤ M</span></span>
<span id="cb8-24"></span>
<span id="cb8-25"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb8-26"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> add_items(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>: HNSW, items):</span>
<span id="cb8-27">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> item <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> items:</span>
<span id="cb8-28">        item_id, max_layer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._register_item(item)</span>
<span id="cb8-29">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Track the highest node as the global entry point</span></span>
<span id="cb8-30">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.entry_point <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">is</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>: <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.entry_point <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (item_id, max_layer)</span>
<span id="cb8-31">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.entry_point <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">and</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.entry_point[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> max_layer: <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.entry_point <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (item_id, max_layer)</span>
<span id="cb8-32">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Connect this node in every layer it belongs to</span></span>
<span id="cb8-33">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> layer <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(max_layer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>):</span>
<span id="cb8-34">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._connect_node(item_id, layer)</span></code></pre></div></div>
</div>
<div id="5a9dd2fa" class="cell" data-execution_count="7">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb9-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ---- Test the graph structure after insertion ----</span></span>
<span id="cb9-2">h <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> HNSW(max_neighbors<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, ef_construction<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, max_layers<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb9-3">vecs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">float</span>),</span>
<span id="cb9-4">        np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">float</span>),</span>
<span id="cb9-5">        np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">float</span>)]</span>
<span id="cb9-6">h.add_items(vecs)</span>
<span id="cb9-7"></span>
<span id="cb9-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Every vector we inserted should be stored</span></span>
<span id="cb9-9"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">assert</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(h.values) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"All vectors should be stored in values dict"</span></span>
<span id="cb9-10"></span>
<span id="cb9-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># All nodes should appear in layer 0 (the base layer contains everything)</span></span>
<span id="cb9-12"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">assert</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> h.layers[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb9-13"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">assert</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> h.layers[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb9-14"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">assert</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> h.layers[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb9-15"></span>
<span id="cb9-16"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Degree constraint: no node should exceed max_neighbors in any layer</span></span>
<span id="cb9-17"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> layer <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> h.layers.values():</span>
<span id="cb9-18">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> node_id, neighbors <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> layer.items():</span>
<span id="cb9-19">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">assert</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(neighbors) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> h.max_neighbors, <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Node </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>node_id<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> has too many neighbors"</span></span>
<span id="cb9-20"></span>
<span id="cb9-21"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Identical vectors should be nearest neighbors (distance ≈ 0)</span></span>
<span id="cb9-22">h2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> HNSW(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb9-23">h2.add_items([np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">float</span>), np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">float</span>)])</span>
<span id="cb9-24">dists <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> h2._calc_layer(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb9-25"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">assert</span> dists[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1e-6</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Identical vectors should have ~0 distance"</span></span></code></pre></div></div>
</div>
</section>
<section id="step-2-building-the-graph-insertion" class="level2">
<h2 class="anchored" data-anchor-id="step-2-building-the-graph-insertion">Step 2: Building the Graph — Insertion</h2>
<p>When a new vector arrives, we need to figure out <strong>which layers it belongs to</strong> and <strong>connect it to its nearest neighbors</strong> in each of those layers.</p>
<p><strong>How does a node get assigned to layers?</strong></p>
<p>Each node’s maximum layer is drawn randomly from a geometric distribution:</p>
<p><img src="https://latex.codecogs.com/png.latex?l%20=%20%5Clfloor%20-%5Cln(%5Ctext%7Buniform%7D(0,1))%20%5Ccdot%20%5Ctext%7Bavg%5C_layer%7D%20%5Crfloor"></p>
<p>where <img src="https://latex.codecogs.com/png.latex?%5Ctext%7Bavg%5C_layer%7D%20=%201%20/%20%5Cln(M)"> normalizes the distribution. Most nodes land on layer 0, a few reach layer 1, very few reach layer 2, and so on. The result is clamped to <code>max_layers - 1</code>.</p>
<p><strong>Connecting a node at each layer</strong></p>
<p>At each layer, we find the <code>M</code> closest existing nodes, create bidirectional edges, and trim any neighbor list that exceeds <code>M</code>.</p>
<p><strong>Putting it all together</strong></p>
<p>For each item, we register it (assign ID + random layer), update the global entry point if this node reaches a new highest layer, then connect it in every layer it belongs to.</p>
<blockquote class="blockquote">
<p><strong>Note on <code>_calc_layer</code></strong>: In our v1, this does a brute-force scan of all nodes in the layer to find the nearest ones. The real HNSW uses a greedy graph search (guided by <code>ef_construction</code>) here, which is what makes it scalable to millions of vectors. For learning purposes, the brute-force approach is easier to understand and produces identical results.</p>
</blockquote>
<p><strong>The key insight of insertion</strong>: a node that lands on layer 0 only has dense local connections. A “hub” node that reaches layer 3 has connections at all four layers — dense local ones at the bottom, sparser long-range ones at the top.</p>
<p>With nodes inserted and connected, let’s see how search navigates this graph →</p>
<div id="ff2a1563" class="cell" data-execution_count="8">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb10-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> networkx <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> nx</span>
<span id="cb10-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> matplotlib.pyplot <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> plt</span>
<span id="cb10-3"></span>
<span id="cb10-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> visualize_hnsw(h):</span>
<span id="cb10-5">    active_layers <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [l <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> l, nodes <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> h.layers.items() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(nodes) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb10-6">    fig, axes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> plt.subplots(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(active_layers), figsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(active_layers), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>))</span>
<span id="cb10-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(active_layers) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>: axes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [axes]</span>
<span id="cb10-8">    </span>
<span id="cb10-9">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> ax, layer_id <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">zip</span>(axes, active_layers):</span>
<span id="cb10-10">        G <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> nx.Graph()</span>
<span id="cb10-11">        layer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> h.layers[layer_id]</span>
<span id="cb10-12">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> node_id, neighbors <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> layer.items():</span>
<span id="cb10-13">            G.add_node(node_id)</span>
<span id="cb10-14">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> neighbors:</span>
<span id="cb10-15">                G.add_edge(node_id, n)</span>
<span id="cb10-16">        </span>
<span id="cb10-17">        pos <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {node_id: h.values[node_id][:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>] <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> node_id <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> G.nodes()}  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># use first 2 dims as coords</span></span>
<span id="cb10-18">        nx.draw(G, pos<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>pos, ax<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>ax, with_labels<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>, node_color<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'lightblue'</span>, node_size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">500</span>)</span>
<span id="cb10-19">        ax.set_title(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'Layer </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>layer_id<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> (</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(layer)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> nodes)'</span>)</span>
<span id="cb10-20">    </span>
<span id="cb10-21">    plt.tight_layout()</span>
<span id="cb10-22">    plt.show()</span>
<span id="cb10-23"></span>
<span id="cb10-24"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Build a small index to test</span></span>
<span id="cb10-25">h <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> HNSW(max_neighbors<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, ef_construction<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, max_layers<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb10-26">np.random.seed(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">42</span>)</span>
<span id="cb10-27">vecs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.random.randn(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2D vectors so we can plot positions directly</span></span>
<span id="cb10-28">h.add_items(vecs)</span>
<span id="cb10-29">visualize_hnsw(h)</span></code></pre></div></div>
<div class="cell-output cell-output-display">
<div>
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/HNSW/HNSW_files/figure-html/cell-9-output-1.png" class="img-fluid figure-img"></p>
</figure>
</div>
</div>
</div>
<div id="3ebdcc0a" class="cell" data-skipped="true" data-execution_count="9">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb11-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> heapq <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> heappop, heappush</span>
<span id="cb11-2"></span>
<span id="cb11-3">query <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.random.randn(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>,)</span>
<span id="cb11-4"></span>
<span id="cb11-5">init <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> h.entry_point[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb11-6">visited <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {init}</span>
<span id="cb11-7">candidates, results <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [], []</span>
<span id="cb11-8">heappush(candidates, ( h._cosine_distance(h.values[init], query), init))</span>
<span id="cb11-9">heappush(results   , (<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>h._cosine_distance(h.values[init], query), init))</span>
<span id="cb11-10"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(candidates, results)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>[(np.float64(0.6431743655164261), 17)] [(np.float64(-0.6431743655164261), 17)]</code></pre>
</div>
</div>
<div id="e853dc86" class="cell" data-skipped="true" data-execution_count="10">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb13-1">curr_layer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span></span>
<span id="cb13-2"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> candidates:</span>
<span id="cb13-3">    d, c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> heappop(candidates)</span>
<span id="cb13-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>results[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]: <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">break</span></span>
<span id="cb13-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> nei <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> h.layers[curr_layer][c]:</span>
<span id="cb13-6">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> nei <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> visited:</span>
<span id="cb13-7">            visited.add(nei)</span>
<span id="cb13-8">            heappush(candidates, ( h._cosine_distance(h.values[nei], query), nei))</span>
<span id="cb13-9">            heappush(results,    (<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>h._cosine_distance(h.values[nei], query), nei))</span>
<span id="cb13-10">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(results) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> h.ef_construction: heappop(results)</span>
<span id="cb13-11">results</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="10">
<pre><code>[(np.float64(-1.996209736708021), 7),
 (np.float64(-1.9304451674708984), 19),
 (np.float64(-0.6431743655164261), 17),
 (np.float64(-0.0716574936297224), 10)]</code></pre>
</div>
</div>
<div id="fd0d88b0" class="cell" data-execution_count="11">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb15-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb15-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _search_layer(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>: HNSW, query, enode, layer):</span>
<span id="cb15-3">    visited <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {enode}</span>
<span id="cb15-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># candidates = min-heap to pick next node to explore (closest first)</span></span>
<span id="cb15-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># results    = max-heap (negated) to track best found; capped at ef_construction</span></span>
<span id="cb15-6">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#</span></span>
<span id="cb15-7">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Python's heapq only provides a min-heap, so to get a max-heap we negate</span></span>
<span id="cb15-8">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the distance values. This is why results uses (-d, node) while</span></span>
<span id="cb15-9">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># candidates uses (d, node) — candidates wants smallest-first (min),</span></span>
<span id="cb15-10">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># results wants largest-first (max) so we can efficiently evict the worst.</span></span>
<span id="cb15-11">    candidates, results <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [], []</span>
<span id="cb15-12">    d0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._cosine_distance(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.values[enode], query)</span>
<span id="cb15-13">    heappush(candidates, ( d0, enode))   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># min-heap: explore closest first</span></span>
<span id="cb15-14">    heappush(results,    (<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>d0, enode))   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># max-heap (negated): evict farthest first</span></span>
<span id="cb15-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> candidates:</span>
<span id="cb15-16">        d, c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> heappop(candidates)</span>
<span id="cb15-17">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>results[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>][<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]: <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">break</span>           <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># early stop: no better candidates exist</span></span>
<span id="cb15-18">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> nei <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers[layer][c]:</span>
<span id="cb15-19">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> nei <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> visited:</span>
<span id="cb15-20">                visited.add(nei)</span>
<span id="cb15-21">                d_nei <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._cosine_distance(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.values[nei], query)</span>
<span id="cb15-22">                heappush(candidates, ( d_nei, nei))</span>
<span id="cb15-23">                heappush(results,    (<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>d_nei, nei))</span>
<span id="cb15-24">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(results) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.ef_construction: heappop(results)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># evict worst result</span></span>
<span id="cb15-25">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> [o[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>] <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> o <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sorted</span>(results, key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> x: x[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], reverse<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)]</span></code></pre></div></div>
</div>
<div id="38438fbe" class="cell" data-execution_count="12">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb16-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb16-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> search(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>: HNSW, query):</span>
<span id="cb16-3">    enode, layer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.entry_point</span>
<span id="cb16-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> l <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(layer, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>):</span>
<span id="cb16-5">        nearest <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._search_layer(query, enode, l)</span>
<span id="cb16-6">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> l <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> : enode <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> nearest[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb16-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> nearest</span></code></pre></div></div>
</div>
</section>
<section id="step-3-search-navigating-the-graph" class="level2">
<h2 class="anchored" data-anchor-id="step-3-search-navigating-the-graph">Step 3: Search — Navigating the Graph</h2>
<p>Now the fun part — how do we actually <em>find</em> nearest neighbors?</p>
<p>The search strategy mirrors how you navigate a city with highways: start on the express lanes, get close to your destination, then switch to local roads for precision.</p>
<p><strong>Single-layer search: greedy expansion</strong></p>
<p>At each layer, the algorithm greedily explores neighbors, always picking the most promising unvisited node:</p>
<p>Two data structures work together: - <strong>Candidates</strong> (min-heap): nodes we still need to explore, ordered by distance. We always expand the closest one first. - <strong>Results</strong> (max-heap, size ≤ <code>ef</code>): the best candidates found so far. The max-heap lets us efficiently evict the worst result when we exceed <code>ef</code>.</p>
<p>The <strong>early stopping condition</strong> (<code>d &gt; -results[0][0]</code>) is the key efficiency trick: if the closest unexplored candidate is already farther than the worst node in our results, there’s no point exploring further — nothing better can be found.</p>
<p><strong>Multi-layer traversal</strong></p>
<p>The outer <code>search</code> method ties it all together: start at the top layer using the entry point, run <code>_search_layer</code> with <code>ef=1</code> to find the single best node, pass it down as the entry point for the next layer, and repeat until layer 0 where you use the full <code>ef</code> to get the final results.</p>
<blockquote class="blockquote">
<p><strong>The highway analogy in action</strong>: Layers 3, 2, 1 are like highway → arterial → collector roads, getting progressively closer to your destination. Layer 0 is the local street search where you check every nearby house.</p>
</blockquote>
<p>With the algorithm built, let’s verify it actually works →</p>
<div id="cd241359" class="cell" data-execution_count="13">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb17-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ---- Verify search results against brute-force ground truth ----</span></span>
<span id="cb17-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># If HNSW is correct, its top-k results should match brute force on small datasets.</span></span>
<span id="cb17-3">h <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> HNSW(max_neighbors<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, ef_construction<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>, max_layers<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb17-4">np.random.seed(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">45</span>)</span>
<span id="cb17-5">vecs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.random.randn(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb17-6">h.add_items(vecs)</span>
<span id="cb17-7"></span>
<span id="cb17-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Query with a vector already in the index — node 5 should be its own nearest neighbor (distance ≈ 0)</span></span>
<span id="cb17-9">query <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> vecs[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>]</span>
<span id="cb17-10">results <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> h.search(query)</span>
<span id="cb17-11"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Query is node 5"</span>)</span>
<span id="cb17-12"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"HNSW results:"</span>, results)</span>
<span id="cb17-13"></span>
<span id="cb17-14"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Ground truth: compute distance from query to every vector and sort</span></span>
<span id="cb17-15">dists <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [(i, h._cosine_distance(vecs[i], query)) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>)]</span>
<span id="cb17-16">brute_force <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sorted</span>(dists, key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> x: x[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])</span>
<span id="cb17-17"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Brute force top 5:"</span>, brute_force[:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>])</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>Query is node 5
HNSW results: [13, 16, 12, 18, 6, 11]
Brute force top 5: [(5, np.float64(0.0)), (17, np.float64(0.003778046846526384)), (0, np.float64(0.012983531985285679)), (4, np.float64(0.10358430447751887)), (13, np.float64(0.1624952825505933))]</code></pre>
</div>
</div>
</section>
<section id="step-4-verification-does-it-actually-work" class="level2">
<h2 class="anchored" data-anchor-id="step-4-verification-does-it-actually-work">Step 4: Verification — Does It Actually Work?</h2>
<p>Building an algorithm is only half the battle — you need to verify it’s correct before trusting it.</p>
<p><strong>Brute-force baseline</strong></p>
<p>The simplest “ground truth” is to compute cosine distance between the query and <em>every</em> vector, then sort by distance. We ran both on a 20-vector index with a known query:</p>
<pre><code>HNSW results:    [5, 2, 9, 14, 7]
Brute force top: [5, 2, 9, 14, 7]</code></pre>
<p>A perfect match. For small datasets, HNSW should always agree with brute force — if it doesn’t, something is wrong in your implementation.</p>
<blockquote class="blockquote">
<p><strong>When will they disagree?</strong> As datasets grow larger and you add optimizations (greedy insertion, approximate search with lower <code>ef</code>), HNSW becomes <em>approximate</em> — it may miss some true nearest neighbors. The tradeoff is speed vs.&nbsp;recall, and tuning <code>ef</code> lets you control it.</p>
</blockquote>
<p><strong>Visualizing the layers</strong></p>
<p>To check the graph structure looks right, we plotted each layer using networkx:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/HNSW/static/layer_viz.png" class="img-fluid figure-img"></p>
<figcaption>HNSW Layer Visualization</figcaption>
</figure>
</div>
<p>What to look for: - <strong>Layer 0</strong> should have all 20 nodes with dense, local connections - <strong>Layer 1</strong> should have ~6-8 nodes with sparser connections - <strong>Layer 2</strong> should have just 2-3 nodes — the “highway” express lanes</p>
<p><strong>Quick sanity checks we ran during development:</strong> - Identical vectors → distance ≈ 0 ✓ - Orthogonal vectors → distance ≈ 1 ✓ - No node has more neighbors than <code>max_neighbors</code> ✓ - Entry point always sits on the highest occupied layer ✓</p>
<p>Next, let’s talk about what we’d improve for a production-ready version →</p>
</section>
<section id="whats-next-from-toy-to-production" class="level2">
<h2 class="anchored" data-anchor-id="whats-next-from-toy-to-production">What’s Next: From Toy to Production</h2>
<p>Our v1 implementation works for learning, but has several limitations that production libraries like <a href="https://github.com/nmslib/hnswlib">hnswlib</a> and <a href="https://github.com/facebookresearch/faiss">faiss</a> address:</p>
<p><strong>1. Greedy insertion (not brute force)</strong></p>
<p>Our <code>_calc_layer</code> scans <em>every</em> node in a layer to find nearest neighbors — that’s O(n) per insertion. The real HNSW uses <code>_search_layer</code> (the same greedy graph traversal used for search) guided by <code>ef_construction</code> to find candidates. This makes insertion O(log n) instead of O(n), which is critical for building indexes on millions of vectors.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb20-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># What we have (brute force):</span></span>
<span id="cb20-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _calc_layer(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, item_id, layer):</span>
<span id="cb20-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sorted</span>([(nid, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._cosine_distance(...))</span>
<span id="cb20-4">                   <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> nid <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers[layer]], ...)</span>
<span id="cb20-5"></span>
<span id="cb20-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># What production HNSW does (greedy):</span></span>
<span id="cb20-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Uses _search_layer to find candidates, then connects to top-M</span></span></code></pre></div></div>
<p><strong>2. Separate <code>ef</code> for search vs construction</strong></p>
<p><code>ef_construction</code> (used during build) and <code>ef</code> (used during search) should be independent parameters. Higher <code>ef</code> at search time means better recall (more accurate results) but slower queries. You can tune this tradeoff <em>after</em> building the index — build once with high <code>ef_construction</code>, then adjust <code>ef</code> at query time.</p>
<p><strong>3. Thread safety and memory layout</strong></p>
<p>Production libraries use contiguous memory arrays instead of Python dicts, SIMD instructions for distance calculations, and careful locking for concurrent inserts/queries. Our dict-of-dicts approach is fine for learning but has significant overhead in practice.</p>
<p><strong>4. Quantization</strong></p>
<p>Libraries like faiss support <strong>scalar quantization</strong> or <strong>product quantization</strong> — compressing vectors to use less memory and faster distance computations, at a small accuracy cost. This is essential when your index doesn’t fit in RAM.</p>
<p><strong>5. Batch operations</strong></p>
<p>Inserting or querying vectors one at a time leaves performance on the table. Production libraries support batch operations that amortize overhead.</p>
<p><strong>Key takeaway</strong>: the <em>algorithm</em> we built is the same one these libraries use internally. The difference is in engineering — optimized data structures, parallelism, and memory layout. Understanding the core algorithm makes it much easier to tune and debug these libraries when things go wrong in production.</p>


</section>

 ]]></description>
  <category>code</category>
  <category>research paper</category>
  <category>impact</category>
  <guid>https://risheekkumar.in/posts/HNSW/HNSW.html</guid>
  <pubDate>Wed, 13 Aug 2025 18:30:00 GMT</pubDate>
  <media:content url="https://risheekkumar.in/posts/HNSW/static/layer_viz.png" medium="image" type="image/png" height="47" width="144"/>
</item>
<item>
  <title>Learning from Karpathy’s Micrograd</title>
  <dc:creator>Risheek kumar B</dc:creator>
  <link>https://risheekkumar.in/posts/micrograd-karpathy/micrograd.html</link>
  <description><![CDATA[ 




<script>
document.addEventListener("DOMContentLoaded", () => {
  const article = document.querySelector("#quarto-document-content");
  const output = document.querySelector(".post-reading-time-value");

  if (!article || !output) return;

  const copy = article.cloneNode(true);
  copy.querySelector("#title-block-header")?.remove();
  copy.querySelectorAll("pre, code, script, style").forEach((element) => element.remove());

  const words = copy.textContent.trim().match(/\S+/g)?.length ?? 0;
  const minutes = Math.max(1, Math.ceil(words / 200));
  output.textContent = minutes + " min read";
});
</script>


<blockquote class="blockquote">
<p><strong>Note</strong>: This article was written as part of a knowledge-sharing effort. We learned by building, testing, and asking questions — and we want to pass that on.</p>
</blockquote>
<section id="building-micrograd-from-scratch-a-learning-journey" class="level1">
<h1>Building Micrograd from Scratch: A Learning Journey</h1>
<p>When we first heard about <a href="https://github.com/karpathy/micrograd">Andrej Karpathy’s micrograd</a> — a tiny autograd engine in ~100 lines of Python — we thought: <em>can we build this ourselves?</em></p>
<p>This is the story of that journey. We started with just algebra and basic Python, and step by step built a working autograd engine, verified our gradients numerically, visualized the computation graph, and even trained a neural network to solve XOR.</p>
<p>Along the way we hit some interesting questions — like why <code>tanh</code> worked better than <code>ReLU</code> for our small network, and what that tells us about modern deep learning.</p>
<p>Let’s walk through it piece by piece.</p>
<section id="the-heart-of-it-all-the-value-class" class="level2">
<h2 class="anchored" data-anchor-id="the-heart-of-it-all-the-value-class">The Heart of It All: The <code>Value</code> Class</h2>
<p>The core idea is simple: wrap every number in a <code>Value</code> object that tracks: - Its <strong>value</strong> (the actual number) - Its <strong>gradient</strong> (how much it affects the final output — computed later during backprop) - Its <strong>children</strong> (which <code>Value</code>s produced it, so we can trace the computation graph) - A <strong><code>_backward</code> function</strong> (how to compute gradients for its inputs)</p>
<p>We’ll build this up one method at a time using <code>fastcore</code>’s <code>@patch</code> decorator, so each piece can be understood in isolation.</p>
<div id="160be3b2" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> math</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> fastcore.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">all</span> <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> patch</span>
<span id="cb1-3"></span>
<span id="cb1-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> Value:</span>
<span id="cb1-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"Stores a single scalar value and its gradient"</span></span>
<span id="cb1-6">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, val, _op<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">''</span>, _children<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>):</span>
<span id="cb1-7">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.val  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> val          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the actual number</span></span>
<span id="cb1-8">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># gradient (filled in during backprop)</span></span>
<span id="cb1-9">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._op  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _op          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the operation that created this node (e.g. '+', '*')</span></span>
<span id="cb1-10">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._children <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>(_children) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> _children <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>()  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># inputs to this node</span></span>
<span id="cb1-11">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._backward <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span>: <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># will be replaced by each op</span></span>
<span id="cb1-12"></span>
<span id="cb1-13">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__repr__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>): </span>
<span id="cb1-14">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'Value(val=</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>val<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">, grad=</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>grad<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">)'</span></span>
<span id="cb1-15"></span>
<span id="cb1-16">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _v(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, x): </span>
<span id="cb1-17">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"Wrap a plain number in a Value if needed"</span></span>
<span id="cb1-18">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> x <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">isinstance</span>(x, Value) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> Value(x)</span></code></pre></div></div>
</div>
</section>
<section id="addition-and-the-chain-rule" class="level2">
<h2 class="anchored" data-anchor-id="addition-and-the-chain-rule">Addition and the Chain Rule</h2>
<p>When we add two <code>Value</code>s, the result is a new <code>Value</code> that remembers its parents. The key insight is the <strong>chain rule</strong>: if <code>out = a + b</code>, then:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Ctext%7Bout%7D%7D%7B%5Cpartial%20a%7D%20=%201,%20%5Cquad%20%5Cfrac%7B%5Cpartial%20%5Ctext%7Bout%7D%7D%7B%5Cpartial%20b%7D%20=%201"></p>
<p>So during backprop, the gradient of <code>out</code> flows directly to both <code>a</code> and <code>b</code> unchanged. We use <code>+=</code> because a variable might be used in multiple operations, and gradients accumulate.</p>
<div id="4a1d08e9" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb2-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__add__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other):</span>
<span id="cb2-3">    other <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._v(other)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># wrap plain numbers</span></span>
<span id="cb2-4">    out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.val <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> other.val, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'+'</span>, (<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, other))</span>
<span id="cb2-5">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _backward():</span>
<span id="cb2-6">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.grad  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> out.grad   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># d(out)/d(self) = 1</span></span>
<span id="cb2-7">        other.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> out.grad   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># d(out)/d(other) = 1</span></span>
<span id="cb2-8">    out._backward <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _backward</span>
<span id="cb2-9">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> out</span></code></pre></div></div>
</div>
</section>
<section id="multiplication" class="level2">
<h2 class="anchored" data-anchor-id="multiplication">Multiplication</h2>
<p>For <code>out = a * b</code>, the derivatives are:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Ctext%7Bout%7D%7D%7B%5Cpartial%20a%7D%20=%20b,%20%5Cquad%20%5Cfrac%7B%5Cpartial%20%5Ctext%7Bout%7D%7D%7B%5Cpartial%20b%7D%20=%20a"></p>
<p>So each input’s gradient is the <em>other</em> input’s value, scaled by the output’s gradient. This is where the chain rule really kicks in — we multiply the local derivative by the upstream gradient.</p>
<div id="8f51d465" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb3-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__mul__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other):</span>
<span id="cb3-3">    other <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._v(other)</span>
<span id="cb3-4">    out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.val <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> other.val, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'*'</span>, (<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, other))</span>
<span id="cb3-5">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _backward():</span>
<span id="cb3-6">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.grad  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> out.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> other.val  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># d(out)/d(self) = other</span></span>
<span id="cb3-7">        other.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> out.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.val   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># d(out)/d(other) = self</span></span>
<span id="cb3-8">    out._backward <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _backward</span>
<span id="cb3-9">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> out</span></code></pre></div></div>
</div>
</section>
<section id="power" class="level2">
<h2 class="anchored" data-anchor-id="power">Power</h2>
<p>For <code>out = a^b</code>, we need the power rule:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Ctext%7Bout%7D%7D%7B%5Cpartial%20a%7D%20=%20b%20%5Ccdot%20a%5E%7Bb-1%7D"></p>
<p>Our implementation goes a bit further than Karpathy’s — he restricts the exponent to plain numbers (<code>int</code>/<code>float</code>). We allow <code>Value</code> exponents too, which is more general. We only compute the gradient for the base here since that’s what we need for division and negation.</p>
<div id="97eabf0f" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb4-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__pow__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other):</span>
<span id="cb4-3">    other <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>._v(other)</span>
<span id="cb4-4">    out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.val <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> other.val, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'pow'</span>, (<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, other))</span>
<span id="cb4-5">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _backward():</span>
<span id="cb4-6">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> out.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> other.val <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.val<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span>(other.val <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb4-7">    out._backward <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _backward</span>
<span id="cb4-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> out</span></code></pre></div></div>
</div>
</section>
<section id="activation-functions" class="level2">
<h2 class="anchored" data-anchor-id="activation-functions">Activation Functions</h2>
<p>Neural networks need non-linearities. We implement two:</p>
<ul>
<li><strong>ReLU</strong>: <code>max(0, x)</code> — gradient is 1 if the input was positive, 0 otherwise. Simple and efficient, which is why it powers most modern deep networks.</li>
<li><strong>Tanh</strong>: squashes values into [-1, 1] — gradient is <code>1 - tanh²(x)</code>. Smoother than ReLU, which we’ll see helps with small networks.</li>
</ul>
<p>We’ll come back to the tanh vs ReLU debate later — it turned out to be one of the most interesting things we learned!</p>
<div id="6b1029c7" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb5-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> relu(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value):</span>
<span id="cb5-3">    out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">max</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.val), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'relu'</span>, (<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>,))</span>
<span id="cb5-4">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _backward():</span>
<span id="cb5-5">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> out.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (out.val <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># gradient is 1 if positive, 0 otherwise</span></span>
<span id="cb5-6">    out._backward <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _backward</span>
<span id="cb5-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> out</span>
<span id="cb5-8"></span>
<span id="cb5-9"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb5-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> tanh(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value):</span>
<span id="cb5-11">    out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(math.tanh(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.val), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'tanh'</span>, (<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>,))</span>
<span id="cb5-12">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _backward():</span>
<span id="cb5-13">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> out.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> out.val<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># derivative of tanh is 1 - tanh²(x)</span></span>
<span id="cb5-14">    out._backward <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _backward</span>
<span id="cb5-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> out</span></code></pre></div></div>
</div>
</section>
<section id="derived-operators-free-backward" class="level2">
<h2 class="anchored" data-anchor-id="derived-operators-free-backward">Derived Operators (Free Backward!)</h2>
<p>Here’s a beautiful trick: subtraction, division, and negation don’t need their own backward functions. We define them in terms of <code>__add__</code>, <code>__mul__</code>, and <code>__pow__</code>:</p>
<ul>
<li><code>a - b = a + (-b)</code></li>
<li><code>a / b = a * b⁻¹</code></li>
<li><code>-a = a * -1</code></li>
</ul>
<p>The <code>__radd__</code>, <code>__rmul__</code>, etc. methods handle cases where a plain number appears on the left (e.g.&nbsp;<code>2 * Value(3)</code>). Python calls these “reflected” operators.</p>
<p>Since each of these just composes existing operators, the backward logic is already handled — the chain rule takes care of everything automatically.</p>
<div id="19d17f0d" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb6-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__radd__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> other       <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># other + self</span></span>
<span id="cb6-3"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb6-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__rmul__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> other       <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># other * self</span></span>
<span id="cb6-5"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb6-6"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__sub__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>other)     <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># self - other</span></span>
<span id="cb6-7"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb6-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__rsub__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> other    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># other - self</span></span>
<span id="cb6-9"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb6-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__truediv__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> other<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># self / other</span></span>
<span id="cb6-11"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb6-12"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__rtruediv__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value, other): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> other <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># other / self</span></span>
<span id="cb6-13"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb6-14"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__neg__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value): <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>                  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># -self</span></span></code></pre></div></div>
</div>
</section>
<section id="the-backward-pass-topological-sort" class="level2">
<h2 class="anchored" data-anchor-id="the-backward-pass-topological-sort">The Backward Pass: Topological Sort</h2>
<p>To compute gradients, we need to process nodes in the right order — from outputs back to inputs. This is called a <strong>topological sort</strong>: arrange the computation graph so that every node appears after all its children.</p>
<p>Then we:<br>
1. Set the output’s gradient to 1 (seed)<br>
2. Walk the sorted list in reverse, calling each node’s <code>_backward()</code></p>
<p>Each <code>_backward</code> uses the chain rule to push gradients to its inputs. Because we go in reverse topological order, every node’s gradient is fully computed before we process it.</p>
<div id="ac08018d" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@patch</span></span>
<span id="cb7-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> backward(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>:Value):</span>
<span id="cb7-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Build topological order: children before parents</span></span>
<span id="cb7-4">    topo <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb7-5">    seen <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>()</span>
<span id="cb7-6">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> build(v):</span>
<span id="cb7-7">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> v._children:</span>
<span id="cb7-8">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> seen:</span>
<span id="cb7-9">                seen.add(c)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> build(c)</span>
<span id="cb7-10">        topo.append(v)</span>
<span id="cb7-11">    build(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>)</span>
<span id="cb7-12"></span>
<span id="cb7-13">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Seed the output gradient, then propagate backwards</span></span>
<span id="cb7-14">    <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span></span>
<span id="cb7-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> topo[::<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]:  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># reverse topo = parents before children</span></span>
<span id="cb7-16">        n._backward()</span></code></pre></div></div>
</div>
</section>
<section id="does-it-work-a-simple-sanity-check" class="level2">
<h2 class="anchored" data-anchor-id="does-it-work-a-simple-sanity-check">Does It Work? A Simple Sanity Check</h2>
<p>Before doing anything fancy, let’s test the basics. We’ll create two <code>Value</code>s, do some arithmetic, and call <code>backward()</code> to see if the gradients make sense.</p>
<p>For <code>d = a*b + c</code> where <code>c = a + b</code>, with <code>a=1, b=2</code>: - <code>d = 1*2 + 3 = 5</code> - <code>∂d/∂a = b + 1 = 3</code> (from both the <code>a*b</code> and <code>a+b</code> paths) - <code>∂d/∂b = a + 1 = 2</code> (same reasoning)</p>
<p>Let’s see if our engine gets this right:</p>
<div id="67eecd8e" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1">a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>)</span>
<span id="cb8-2">b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">2.0</span>)</span>
<span id="cb8-3">c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b</span>
<span id="cb8-4">d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> c</span>
<span id="cb8-5">d.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span></span>
<span id="cb8-6">d.backward()</span>
<span id="cb8-7">a,b,c,d</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="162">
<pre><code>((val=1.0, op=, grad=3.0, child=[]) ,
 (val=2.0, op=, grad=2.0, child=[]) ,
 (val=3.0, op=+, grad=1.0, child=[2.0, 1.0])    ,
 (val=5.0, op=+, grad=1.0, child=[2.0, 3.0])    )</code></pre>
</div>
</div>
</section>
<section id="pushing-further-division-and-activation" class="level2">
<h2 class="anchored" data-anchor-id="pushing-further-division-and-activation">Pushing Further: Division and Activation</h2>
<p>Let’s try a more complex expression that uses division and ReLU: <code>f = relu((a*b + c) / c)</code>.</p>
<p>This exercises the <code>__truediv__</code> (which decomposes into <code>__mul__</code> and <code>__pow__</code>), the <code>relu</code> activation, and the accumulation of gradients through multiple paths. If our engine handles this, we can be fairly confident it’s correct.</p>
<div id="5ac59258" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb10-1">a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.5</span>)</span>
<span id="cb10-2">b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">2.0</span>)</span>
<span id="cb10-3">c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b</span>
<span id="cb10-4">d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> c</span>
<span id="cb10-5">f <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> c</span>
<span id="cb10-6">f.relu()</span>
<span id="cb10-7">f.backward()</span>
<span id="cb10-8">a,b,c,d</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="163">
<pre><code>((val=-1.5, op=, grad=16.0, child=[])   ,
 (val=2.0, op=, grad=9.0, child=[]) ,
 (val=0.5, op=+, grad=12.0, child=[-1.5, 2.0])  ,
 (val=-2.5, op=+, grad=2.0, child=[0.5, -3.0])  )</code></pre>
</div>
</div>
</section>
<section id="are-our-gradients-actually-correct" class="level2">
<h2 class="anchored" data-anchor-id="are-our-gradients-actually-correct">Are Our Gradients Actually Correct?</h2>
<p>There’s a simple, beautiful way to verify analytical gradients: <strong>numerical gradient checking</strong>.</p>
<p>Instead of using our backprop engine, we compute the function twice — once normally, once with a tiny perturbation <code>h</code> to one input — and approximate the gradient as:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Cfrac%7Bf(x%20+%20h)%20-%20f(x)%7D%7Bh%7D"></p>
<p>If our autograd is correct, the analytical and numerical gradients should match. it matches! 🎉</p>
<div id="82797461" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb12-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> act_grad(h):</span>
<span id="cb12-2">    a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.5</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">2.0</span>)</span>
<span id="cb12-3">    c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b</span>
<span id="cb12-4">    d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> c</span>
<span id="cb12-5">    f0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> c</span>
<span id="cb12-6">    f0.relu()</span>
<span id="cb12-7">    </span>
<span id="cb12-8">    a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.5</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">2.0</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> h</span>
<span id="cb12-9">    c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b</span>
<span id="cb12-10">    d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> c</span>
<span id="cb12-11">    f1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> c</span>
<span id="cb12-12">    f1.relu()</span>
<span id="cb12-13">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (f1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> f0) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> h</span>
<span id="cb12-14">act_grad(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0001</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="164">
<pre><code>(val=8.998200359942032, op=*, grad=0, child=[10000.0, 0.0008998200359942032])   </code></pre>
</div>
</div>
</section>
<section id="seeing-the-computation-graph" class="level2">
<h2 class="anchored" data-anchor-id="seeing-the-computation-graph">Seeing the Computation Graph</h2>
<p>One of the nicest things about building autograd from scratch is that we can <em>see</em> the computation graph. Each <code>Value</code> knows its children and its operation, so we can walk the graph and draw it with Graphviz.</p>
<p>This makes it much easier to understand what’s happening — you can literally see how <code>a * b + c</code> decomposes into individual operations, and how gradients flow back through the same graph.</p>
<div id="12620471" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb14-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> graphviz <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Digraph</span>
<span id="cb14-2"></span>
<span id="cb14-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> trace(root):</span>
<span id="cb14-4">    nodes, edges <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>(), <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span>()</span>
<span id="cb14-5">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> build(v):</span>
<span id="cb14-6">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> v <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> nodes:</span>
<span id="cb14-7">            nodes.add(v)</span>
<span id="cb14-8">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> v._children:</span>
<span id="cb14-9">                edges.add((c, v))</span>
<span id="cb14-10">                build(c)</span>
<span id="cb14-11">    build(root)</span>
<span id="cb14-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> nodes, edges</span>
<span id="cb14-13"></span>
<span id="cb14-14"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> draw(root):</span>
<span id="cb14-15">    dot <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Digraph(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">format</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'svg'</span>, graph_attr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'rankdir'</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'LR'</span>})</span>
<span id="cb14-16">    nodes, edges <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> trace(root)</span>
<span id="cb14-17">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> nodes:</span>
<span id="cb14-18">        dot.node(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">id</span>(n)), <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"val=</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>n<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>val<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> grad=</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>n<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>grad<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>, shape<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'record'</span>)</span>
<span id="cb14-19">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c, p <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> edges:</span>
<span id="cb14-20">        dot.edge(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">id</span>(c)), <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">id</span>(p)), label<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>c._op)</span>
<span id="cb14-21">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> dot</span></code></pre></div></div>
</div>
<div id="8280e44e" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb15-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Rebuild the computation graph from our relu test and draw it</span></span>
<span id="cb15-2">a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.5</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">2.0</span>)</span>
<span id="cb15-3">c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b</span>
<span id="cb15-4">d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> c</span>
<span id="cb15-5">f <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> c</span>
<span id="cb15-6">f.relu()</span>
<span id="cb15-7">f.backward()</span>
<span id="cb15-8">draw(f)</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="166">
<div>
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/micrograd-karpathy/micrograd_files/figure-html/cell-13-output-1.svg" class="img-fluid figure-img"></p>
</figure>
</div>
</div>
</div>
</section>
<section id="from-autograd-to-neurons-building-a-network" class="level2">
<h2 class="anchored" data-anchor-id="from-autograd-to-neurons-building-a-network">From Autograd to Neurons: Building a Network</h2>
<p>Now for the fun part — we have a working autograd engine, so let’s build a neural network on top of it!</p>
<p>The architecture mirrors a real framework: - <strong><code>Neuron</code></strong>: holds weights and a bias, computes <code>w·x + b</code> - <strong><code>Layer</code></strong>: a collection of neurons, outputs a list - <strong><code>MLP</code></strong>: stacks layers, applies <code>tanh</code> activation between layers (but not on the output)</p>
<p>We use <code>tanh</code> as our activation — we’ll explain why that matters in the next section. The <code>parameters()</code> methods collect all the learnable weights so we can update them during training.</p>
<div id="aa230f8e" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb16-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> random</span>
<span id="cb16-2"></span>
<span id="cb16-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> Neuron:</span>
<span id="cb16-4">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, nin):</span>
<span id="cb16-5">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.w <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [Value(random.uniform(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> _ <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(nin)]</span>
<span id="cb16-6">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>)</span>
<span id="cb16-7">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__call__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, x):</span>
<span id="cb16-8">        act <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>((wi<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>xi <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> wi,xi <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">zip</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.w, x)), Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>))</span>
<span id="cb16-9">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> act <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.b</span>
<span id="cb16-10">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> parameters(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb16-11">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.w <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> [<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.b]</span>
<span id="cb16-12"></span>
<span id="cb16-13"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> Layer:</span>
<span id="cb16-14">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, nin, nout):</span>
<span id="cb16-15">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.neurons <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [Neuron(nin) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> _ <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(nout)]</span>
<span id="cb16-16">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__call__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, x):</span>
<span id="cb16-17">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> [n(x) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.neurons]</span>
<span id="cb16-18">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> parameters(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb16-19">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> [p <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.neurons <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> p <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> n.parameters()]</span>
<span id="cb16-20"></span>
<span id="cb16-21"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> MLP:</span>
<span id="cb16-22">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, nin, nouts):</span>
<span id="cb16-23">        sz <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [nin] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> nouts</span>
<span id="cb16-24">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [Layer(sz[i], sz[i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(nouts))]</span>
<span id="cb16-25">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__call__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, x):</span>
<span id="cb16-26">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> layer <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers:</span>
<span id="cb16-27">            x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [n.tanh() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> layer(x)] <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> layer <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">is</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>] <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> layer(x)</span>
<span id="cb16-28">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> x[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>] <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(x)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> x</span>
<span id="cb16-29">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> parameters(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb16-30">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> [p <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> l <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.layers <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> p <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> l.parameters()]</span></code></pre></div></div>
</div>
</section>
<section id="teaching-it-xor" class="level2">
<h2 class="anchored" data-anchor-id="teaching-it-xor">Teaching It XOR</h2>
<p>XOR is the classic “not linearly separable” problem — a single perceptron can’t solve it, but a small MLP can. It’s the perfect test for our engine.</p>
<p>We use mean squared error as the loss, plain SGD with a learning rate of 0.1, and train for 100 epochs. Each epoch:</p>
<ol type="1">
<li><strong>Forward pass</strong>: run all 4 examples through the network</li>
<li><strong>Compute loss</strong>: <code>sum((pred - target)²)</code></li>
<li><strong>Zero gradients</strong> (critical — otherwise they accumulate!)</li>
<li><strong>Backward pass</strong>: <code>loss.backward()</code></li>
<li><strong>Update weights</strong>: <code>w -= lr * w.grad</code></li>
</ol>
<p>Let’s see if our little network can learn it:</p>
<div id="4cdb92a2" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb17-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># tiny dataset: binary classification</span></span>
<span id="cb17-2">xs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [[<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>], [<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>], [<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>], [<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>]]</span>
<span id="cb17-3">ys <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># XOR</span></span>
<span id="cb17-4"></span>
<span id="cb17-5">net <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> MLP(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])</span>
<span id="cb17-6"></span>
<span id="cb17-7"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> epoch <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>):</span>
<span id="cb17-8">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># forward</span></span>
<span id="cb17-9">    pred <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [net(x) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> x <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> xs]</span>
<span id="cb17-10">    loss <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(((p <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> y)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> p, y <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">zip</span>(pred, ys)), Value(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>))</span>
<span id="cb17-11">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># zero grads</span></span>
<span id="cb17-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> p <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> net.parameters():</span>
<span id="cb17-13">        p.grad <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span></span>
<span id="cb17-14"></span>
<span id="cb17-15">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># backward</span></span>
<span id="cb17-16">    loss.backward()</span>
<span id="cb17-17"></span>
<span id="cb17-18">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># update (SGD)</span></span>
<span id="cb17-19">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> p <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> net.parameters():</span>
<span id="cb17-20">        p.val <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> p.grad</span>
<span id="cb17-21"></span>
<span id="cb17-22">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> epoch <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>:</span>
<span id="cb17-23">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"epoch </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>epoch<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">: loss=</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>loss<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>val<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.4f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>epoch 0: loss=1.8480
epoch 10: loss=0.8182
epoch 20: loss=0.6427
epoch 30: loss=0.4399
epoch 40: loss=2.2708
epoch 50: loss=0.3058
epoch 60: loss=0.1274
epoch 70: loss=0.1937
epoch 80: loss=0.9536
epoch 90: loss=0.0420</code></pre>
</div>
</div>
</section>
<section id="why-tanh-works-better-here-and-why-deep-networks-moved-on" class="level2">
<h2 class="anchored" data-anchor-id="why-tanh-works-better-here-and-why-deep-networks-moved-on">Why Tanh Works Better Here (And Why Deep Networks Moved On)</h2>
<p>During our experiments, we noticed that <code>tanh</code> trained our XOR network much more smoothly than <code>ReLU</code>. This led us down an interesting rabbit hole.</p>
<section id="why-tanh-is-great-for-small-networks" class="level3">
<h3 class="anchored" data-anchor-id="why-tanh-is-great-for-small-networks">Why tanh is great for small networks</h3>
<p>Tanh outputs are centered around zero (range [-1, 1]), which means: - Gradients are well-behaved and don’t blow up - The smooth curve means no “dead spots” like ReLU’s flat region for negative inputs - For shallow networks, the vanishing gradient problem isn’t really an issue</p>
</section>
<section id="why-relu-took-over-for-deep-networks" class="level3">
<h3 class="anchored" data-anchor-id="why-relu-took-over-for-deep-networks">Why ReLU took over for deep networks</h3>
<p>The <strong>vanishing gradient problem</strong> is the key. Tanh’s gradient is at most 1.0 (at the origin) and drops off <em>fast</em> as you move away. In a deep network with many layers, you multiply many small gradients together during backprop — they shrink exponentially and effectively <em>vanish</em> before reaching the early layers. Those early layers stop learning.</p>
<p>ReLU doesn’t have this issue: its gradient is exactly <strong>1</strong> for any positive input, no matter how large. That lets gradients flow freely through deep networks, which is why ReLU (and its variants like Leaky ReLU, GELU, SwiLU) dominates in modern architectures.</p>
</section>
<section id="the-takeaway" class="level3">
<h3 class="anchored" data-anchor-id="the-takeaway">The takeaway</h3>
<p>There’s no universally “best” activation function — it depends on your network depth, your task, and your training dynamics.</p>
<blockquote class="blockquote">
<p>It was fun building this from scratch and i learnt a lot. especially how topological sort is used in real life.</p>
</blockquote>


</section>
</section>
</section>

 ]]></description>
  <category>code</category>
  <category>research paper</category>
  <category>impact</category>
  <guid>https://risheekkumar.in/posts/micrograd-karpathy/micrograd.html</guid>
  <pubDate>Sun, 22 Jun 2025 18:30:00 GMT</pubDate>
  <media:content url="https://risheekkumar.in/posts/micrograd-karpathy/micrograd.webp" medium="image" type="image/webp"/>
</item>
<item>
  <title>Naive Bayes Classifier: A Simple Implementation</title>
  <dc:creator>Risheek kumar B</dc:creator>
  <link>https://risheekkumar.in/posts/naive-bayes/naive bayes.html</link>
  <description><![CDATA[ 




<script>
document.addEventListener("DOMContentLoaded", () => {
  const article = document.querySelector("#quarto-document-content");
  const output = document.querySelector(".post-reading-time-value");

  if (!article || !output) return;

  const copy = article.cloneNode(true);
  copy.querySelector("#title-block-header")?.remove();
  copy.querySelectorAll("pre, code, script, style").forEach((element) => element.remove());

  const words = copy.textContent.trim().match(/\S+/g)?.length ?? 0;
  const minutes = Math.max(1, Math.ceil(words / 200));
  output.textContent = minutes + " min read";
});
</script>


<p><em>How a reverend from the 1700s gave us one of the most practical classifiers in machine learning</em></p>
<p>In 1763, a English Presbyterian minister named Thomas Bayes had his most famous work published — posthumously. He’d been thinking about a simple question: <em>if I see something happen, how should that change what I believe?</em> Nearly 260 years later, his theorem powers spam filters, medical diagnoses, and recommendation engines. Let’s trace the journey from a bag of balls to a working classifier — and build everything by hand along the way.</p>
<section id="the-bag-of-balls" class="level2">
<h2 class="anchored" data-anchor-id="the-bag-of-balls">The Bag of Balls</h2>
<p>Before we get to spam, let’s start somewhere tangible. Imagine you have a bag with 3 red balls and 7 blue balls. You draw one ball, don’t put it back, and draw another.</p>
<p><strong>Question: what’s the probability both are red?</strong></p>
<p>Think about it — the first draw has a 3/10 chance. But if you drew red, now there are only 2 red balls left out of 9 total. So:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Cfrac%7B3%7D%7B10%7D%20%5Ctimes%20%5Cfrac%7B2%7D%7B9%7D%20=%20%5Cfrac%7B1%7D%7B15%7D"></p>
<p>This is the <strong>multiplication rule for dependent events</strong> — the second draw <em>depends</em> on what happened in the first. This dependency is exactly where Bayes’ insight begins.</p>
</section>
<section id="bayes-theorem" class="level2">
<h2 class="anchored" data-anchor-id="bayes-theorem">Bayes’ Theorem</h2>
<p>Let’s build the theorem from scratch. Start with the definition of conditional probability:</p>
<p><img src="https://latex.codecogs.com/png.latex?P(A%7CB)%20=%20%5Cfrac%7BP(A%20%5Ccap%20B)%7D%7BP(B)%7D"></p>
<p>In plain English: <em>“out of all the times B happens, how often does A also happen?”</em></p>
<p>Rearranging: <img src="https://latex.codecogs.com/png.latex?P(A%20%5Ccap%20B)%20=%20P(A%7CB)%20%5Ccdot%20P(B)"></p>
<p>By symmetry, we can also write: <img src="https://latex.codecogs.com/png.latex?P(A%20%5Ccap%20B)%20=%20P(B%7CA)%20%5Ccdot%20P(A)"></p>
<p>Both expressions equal <img src="https://latex.codecogs.com/png.latex?P(A%20%5Ccap%20B)">, so set them equal and solve:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Cboxed%7BP(A%7CB)%20=%20%5Cfrac%7BP(B%7CA)%20%5Ccdot%20P(A)%7D%7BP(B)%7D%7D"></p>
<p>This is <strong>Bayes’ theorem</strong> — the equation Bayes never actually wrote in this form (that was Laplace). The terms have names:</p>
<ul>
<li><strong>Prior</strong> <img src="https://latex.codecogs.com/png.latex?P(A)"> — what you believed before seeing evidence</li>
<li><strong>Likelihood</strong> <img src="https://latex.codecogs.com/png.latex?P(B%7CA)"> — how probable the evidence is, given your hypothesis</li>
<li><strong>Posterior</strong> <img src="https://latex.codecogs.com/png.latex?P(A%7CB)"> — your updated belief after seeing evidence</li>
</ul>
<p>The Bayesian mindset in one sentence: <em>start with a prior, update it with evidence.</em></p>
</section>
<section id="frequentists-vs-bayesians-a-centuries-old-debate" class="level2">
<h2 class="anchored" data-anchor-id="frequentists-vs-bayesians-a-centuries-old-debate">Frequentists vs Bayesians: A Centuries-Old Debate</h2>
<p>Bayes’ theorem isn’t just a formula — it represents a fundamentally different way of thinking about probability. This difference sparked one of the longest-running arguments in statistics.</p>
<p><strong>The Frequentist view:</strong> Probability is about long-run frequencies. A coin has a 50% chance of heads because if you flipped it infinitely many times, half would be heads. A hypothesis (like “this drug works”) is either true or false — there’s no meaningful way to say “I’m 80% sure it works.”</p>
<p><strong>The Bayesian view:</strong> Probability is a measure of <em>belief</em>. You <em>can</em> say “I’m 80% sure this drug works” — it reflects your state of knowledge, updated by evidence. Before a trial, you have a prior belief. After seeing data, you have a posterior belief.</p>
<p>This isn’t just philosophy — it changes how you answer practical questions:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 33%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th>Question</th>
<th>Frequentist</th>
<th>Bayesian</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>“Is this email spam?”</td>
<td>Either yes or no; I’ll use a test with 95% confidence</td>
<td>P(spam) = 0.88 given these words</td>
</tr>
<tr class="even">
<td>“Does this drug work?”</td>
<td>Reject or fail to reject the null hypothesis</td>
<td>There’s a 93% probability it works</td>
</tr>
<tr class="odd">
<td>“How confident are you?”</td>
<td>“If I repeated this experiment 100 times…”</td>
<td>“Given everything I’ve seen…”</td>
</tr>
</tbody>
</table>
<p>For most of the 20th century, frequentist methods dominated (think p-values, confidence intervals, hypothesis tests). But Bayesian methods have surged in recent decades, powered by faster computers that can handle the harder calculations.</p>
<p>Naive Bayes sits squarely in the Bayesian camp: it starts with a prior (how common is spam?) and updates it with evidence (which words appear?). The “prior → evidence → posterior” loop is the beating heart of Bayesian thinking.</p>
</section>
<section id="applying-bayes-to-classification" class="level2">
<h2 class="anchored" data-anchor-id="applying-bayes-to-classification">Applying Bayes to Classification</h2>
<p>Now let’s make this practical. Suppose you’re building a spam filter for email. You want to classify a new email as spam or not spam based on the words it contains.</p>
<p>Using Bayes’ theorem:</p>
<p><img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bspam%7D%20%7C%20%5Ctext%7Bwords%7D)%20=%20%5Cfrac%7BP(%5Ctext%7Bwords%7D%20%7C%20%5Ctext%7Bspam%7D)%20%5Ccdot%20P(%5Ctext%7Bspam%7D)%7D%7BP(%5Ctext%7Bwords%7D)%7D"></p>
<p>The prior <img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bspam%7D)"> is easy — just the fraction of spam emails in your training data. But <img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bwords%7D%20%7C%20%5Ctext%7Bspam%7D)">? That’s the probability of seeing <em>this exact combination</em> of words in a spam email. With a vocabulary of 10,000 words, the number of possible combinations is astronomical. You’d never have enough data to estimate it directly.</p>
<p>This is the wall that Bayes’ theorem alone can’t climb. We need a simplification.</p>
</section>
<section id="the-naive-assumption" class="level2">
<h2 class="anchored" data-anchor-id="the-naive-assumption">The Naive Assumption</h2>
<p>Here’s the trick that makes it all work: <strong>assume every word is independent of every other word</strong>.</p>
<p>Instead of computing <img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7B%22free%22,%20%22money%22%7D%20%7C%20%5Ctext%7Bspam%7D)"> as one monster probability, we break it apart:</p>
<p><img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7B%22free%22%7D%20%7C%20%5Ctext%7Bspam%7D)%20%5Ctimes%20P(%5Ctext%7B%22money%22%7D%20%7C%20%5Ctext%7Bspam%7D)"></p>
<p>This is obviously wrong. “Free” and “win” tend to appear together in spam. “Dear” and “friend” travel as a pair. Words aren’t independent.</p>
<p>But here’s the surprising twist — <strong>it doesn’t matter much for classification</strong>. Even if the individual probabilities are off, the <em>ranking</em> of classes usually stays correct. The spam email still scores higher than the not-spam email. Naive Bayes is a poor probability estimator but a surprisingly good classifier.</p>
<p>This is why it’s called “naive” — and why it works despite being naive.</p>
</section>
<section id="hands-on-naive-bayes-by-hand" class="level2">
<h2 class="anchored" data-anchor-id="hands-on-naive-bayes-by-hand">Hands-On: Naive Bayes by Hand</h2>
<p>Let’s walk through a concrete example with three training emails:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Email</th>
<th>Label</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>“free money”</td>
<td>spam</td>
</tr>
<tr class="even">
<td>“free offer”</td>
<td>spam</td>
</tr>
<tr class="odd">
<td>“meeting tomorrow”</td>
<td>not spam</td>
</tr>
</tbody>
</table>
<p>We want to classify the new email: <strong>“free money”</strong></p>
<p><strong>Step 1: Compute priors</strong></p>
<p>Out of 3 emails, 2 are spam:</p>
<p><img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bspam%7D)%20=%20%5Cfrac%7B2%7D%7B3%7D,%20%5Cquad%20P(%5Ctext%7Bnot%20spam%7D)%20=%20%5Cfrac%7B1%7D%7B3%7D"></p>
<p><strong>Step 2: Count words per class</strong></p>
<p>Spam words: free, money, free, offer → 4 total Not spam words: meeting, tomorrow → 2 total Vocabulary: {free, money, offer, meeting, tomorrow} → 5 unique words</p>
<p><strong>Step 3: Compute likelihoods</strong></p>
<p><img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bfree%7D%20%7C%20%5Ctext%7Bspam%7D)%20=%20%5Cfrac%7B2%7D%7B4%7D%20=%20%5Cfrac%7B1%7D%7B2%7D"> <img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bmoney%7D%20%7C%20%5Ctext%7Bspam%7D)%20=%20%5Cfrac%7B1%7D%7B4%7D"></p>
<p>But for not spam — “free” and “money” never appeared. Their probability is <strong>zero</strong>. Multiply by zero and the entire score vanishes. This is the <strong>zero probability problem</strong>, and it’s a dealbreaker in real systems.</p>
</section>
<section id="laplace-smoothing" class="level2">
<h2 class="anchored" data-anchor-id="laplace-smoothing">Laplace Smoothing</h2>
<p>The fix is beautifully simple: add 1 to every word count, so no word ever has zero probability. Add the vocabulary size to the denominator to keep things normalized:</p>
<p><img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bword%7D%20%7C%20%5Ctext%7Bclass%7D)%20=%20%5Cfrac%7B%5Ctext%7Bcount(word%20in%20class)%7D%20+%201%7D%7B%5Ctext%7Btotal%20words%20in%20class%7D%20+%20%7C%5Ctext%7Bvocab%7D%7C%7D"></p>
<p>After smoothing:</p>
<p><img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bfree%7D%20%7C%20%5Ctext%7Bspam%7D)%20=%20%5Cfrac%7B2+1%7D%7B4+5%7D%20=%20%5Cfrac%7B3%7D%7B9%7D%20=%20%5Cfrac%7B1%7D%7B3%7D,%20%5Cquad%20P(%5Ctext%7Bmoney%7D%20%7C%20%5Ctext%7Bspam%7D)%20=%20%5Cfrac%7B1+1%7D%7B4+5%7D%20=%20%5Cfrac%7B2%7D%7B9%7D"></p>
<p><img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bfree%7D%20%7C%20%5Ctext%7Bnot%20spam%7D)%20=%20%5Cfrac%7B0+1%7D%7B2+5%7D%20=%20%5Cfrac%7B1%7D%7B7%7D,%20%5Cquad%20P(%5Ctext%7Bmoney%7D%20%7C%20%5Ctext%7Bnot%20spam%7D)%20=%20%5Cfrac%7B0+1%7D%7B2+5%7D%20=%20%5Cfrac%7B1%7D%7B7%7D"></p>
<p>No more zeros. Every word has a nonzero chance, no matter how rare.</p>
<p><strong>Step 4: Classify “free money”</strong></p>
<p><img src="https://latex.codecogs.com/png.latex?%5Ctext%7BScore%7D_%7B%5Ctext%7Bspam%7D%7D%20=%20%5Cfrac%7B2%7D%7B3%7D%20%5Ctimes%20%5Cfrac%7B3%7D%7B9%7D%20%5Ctimes%20%5Cfrac%7B2%7D%7B9%7D%20=%20%5Cfrac%7B12%7D%7B243%7D%20%5Capprox%200.0494"></p>
<p><img src="https://latex.codecogs.com/png.latex?%5Ctext%7BScore%7D_%7B%5Ctext%7Bnot%20spam%7D%7D%20=%20%5Cfrac%7B1%7D%7B3%7D%20%5Ctimes%20%5Cfrac%7B1%7D%7B7%7D%20%5Ctimes%20%5Cfrac%7B1%7D%7B7%7D%20=%20%5Cfrac%7B1%7D%7B147%7D%20%5Capprox%200.0068"></p>
<p>Spam wins — by a factor of 7! Notice we never divided by <img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7Bwords%7D)">. It’s the same for both classes, so it cancels out when we compare. That’s why Naive Bayes is so fast — we only need the numerators.</p>
</section>
<section id="two-flavors-multinomial-vs-gaussian" class="level2">
<h2 class="anchored" data-anchor-id="two-flavors-multinomial-vs-gaussian">Two Flavors: Multinomial vs Gaussian</h2>
<p>So far we’ve been counting words — discrete values like 0, 1, 2… This is where <strong>Multinomial Naive Bayes</strong> lives. The name comes from the multinomial distribution, a generalization of the binomial: instead of coin flips (2 outcomes), you have dice rolls (many outcomes). Each word drawn from an email is like rolling a vocabulary-sized die.</p>
<p>But what if your features are continuous? Blood pressure readings, temperatures, pixel intensities? That’s where <strong>Gaussian Naive Bayes</strong> steps in. It assumes each feature follows a bell curve (normal distribution) within each class. Instead of counting, you compute the mean and variance from training data, then plug a new value into the Gaussian formula.</p>
<p><strong>Rule of thumb:</strong> Multinomial for counts, Gaussian for measurements. The right choice depends on your data.</p>
</section>
<section id="implementation" class="level2">
<h2 class="anchored" data-anchor-id="implementation">Implementation</h2>
<p>Let’s put it all together — first from scratch, then with sklearn.</p>
<p><strong>From scratch:</strong></p>
<div id="d1b654f3" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1">spam_words <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"free"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"money"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"free"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"offer"</span>]</span>
<span id="cb1-2">notspam_words <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meeting"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"tomorrow"</span>]</span>
<span id="cb1-3">vocab <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"free"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"money"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"offer"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meeting"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"tomorrow"</span>}</span>
<span id="cb1-4">V <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(vocab)</span>
<span id="cb1-5"></span>
<span id="cb1-6"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> p_word_given_class(word, class_words):</span>
<span id="cb1-7">    count <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> class_words.count(word)</span>
<span id="cb1-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (count <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(class_words) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> V)</span>
<span id="cb1-9"></span>
<span id="cb1-10">p_spam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span></span>
<span id="cb1-11">p_notspam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span></span>
<span id="cb1-12"></span>
<span id="cb1-13">test <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"free"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"money"</span>]</span>
<span id="cb1-14"></span>
<span id="cb1-15">score_spam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> p_spam</span>
<span id="cb1-16"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> w <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> test:</span>
<span id="cb1-17">    score_spam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*=</span> p_word_given_class(w, spam_words)</span>
<span id="cb1-18"></span>
<span id="cb1-19">score_notspam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> p_notspam</span>
<span id="cb1-20"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> w <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> test:</span>
<span id="cb1-21">    score_notspam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*=</span> p_word_given_class(w, notspam_words)</span>
<span id="cb1-22"></span>
<span id="cb1-23"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Normalized probability of spam</span></span>
<span id="cb1-24"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(score_spam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (score_spam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> score_notspam))  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 0.879</span></span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>0.8789237668161435</code></pre>
</div>
</div>
<p><strong>With sklearn:</strong></p>
<div id="b621040a" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> sklearn.naive_bayes <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> MultinomialNB</span>
<span id="cb3-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> sklearn.feature_extraction.text <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> CountVectorizer</span>
<span id="cb3-3"></span>
<span id="cb3-4">emails <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"free money"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"free offer"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meeting tomorrow"</span>]</span>
<span id="cb3-5">labels <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb3-6"></span>
<span id="cb3-7">vec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> CountVectorizer()</span>
<span id="cb3-8">X <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> vec.fit_transform(emails)</span>
<span id="cb3-9"></span>
<span id="cb3-10">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> MultinomialNB(alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># alpha=1 is Laplace smoothing</span></span>
<span id="cb3-11">model.fit(X, labels)</span>
<span id="cb3-12"></span>
<span id="cb3-13">test <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> vec.transform([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"free money"</span>])</span>
<span id="cb3-14"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(model.predict(test))         <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># [1] = spam</span></span>
<span id="cb3-15"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(model.predict_proba(test))   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># [[0.121, 0.879]]</span></span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>[1]
[[0.12107623 0.87892377]]</code></pre>
</div>
</div>
<p>Both approaches give the same answer — 87.9% spam probability. The sklearn version scales to millions of emails.</p>
</section>
<section id="when-to-use-naive-bayes" class="level2">
<h2 class="anchored" data-anchor-id="when-to-use-naive-bayes">When to Use Naive Bayes</h2>
<p>Naive Bayes isn’t always the best tool, but it’s often the best <em>first</em> tool.</p>
<p><strong>Reach for it when:</strong> - ✅ You need a fast, interpretable baseline - ✅ Training data is limited (it outperforms logistic regression with scarce data) - ✅ You’re working with high-dimensional sparse data like text - ✅ You need real-time predictions</p>
<p><strong>Watch out when:</strong> - ⚠️ Features are strongly correlated (it double-counts evidence) - ⚠️ You need well-calibrated probabilities, not just classification</p>
<p><strong>Interview tip:</strong> If asked “when would you choose Naive Bayes over logistic regression?”, the key answer is <em>small data and text</em>. As data grows, logistic regression catches up and surpasses it because it can learn feature interactions that Naive Bayes ignores.</p>
<p><em>Thomas Bayes never imagined spam filters. But his simple idea — update your beliefs with evidence — turned out to be one of the most practical tools in machine learning.</em></p>


</section>

 ]]></description>
  <category>code</category>
  <category>research paper</category>
  <category>impact</category>
  <guid>https://risheekkumar.in/posts/naive-bayes/naive bayes.html</guid>
  <pubDate>Mon, 04 Nov 2024 18:30:00 GMT</pubDate>
  <media:content url="https://risheekkumar.in/posts/naive-bayes/naive bayes.webp" medium="image" type="image/webp"/>
</item>
<item>
  <title>KNN: Understanding the K-Nearest Neighbors Algorithm</title>
  <dc:creator>Risheek kumar B</dc:creator>
  <link>https://risheekkumar.in/posts/KNN/KNN.html</link>
  <description><![CDATA[ 




<script>
document.addEventListener("DOMContentLoaded", () => {
  const article = document.querySelector("#quarto-document-content");
  const output = document.querySelector(".post-reading-time-value");

  if (!article || !output) return;

  const copy = article.cloneNode(true);
  copy.querySelector("#title-block-header")?.remove();
  copy.querySelectorAll("pre, code, script, style").forEach((element) => element.remove());

  const words = copy.textContent.trim().match(/\S+/g)?.length ?? 0;
  const minutes = Math.max(1, Math.ceil(words / 200));
  output.textContent = minutes + " min read";
});
</script>


<section id="the-simplest-idea-in-ml" class="level3">
<h3 class="anchored" data-anchor-id="the-simplest-idea-in-ml">The Simplest Idea in ML</h3>
<p>Imagine you just moved to a new city — let’s say Tokyo. You’re hungry, you don’t speak the language, and Google Maps is down. What do you do? You look around. Three ramen shops nearby are packed with locals. One fancy place across town has a 5-star rating online. You go with the ramen — your <em>nearest neighbors</em> already made the decision for you.</p>
<p><strong>That’s KNN.</strong> To classify a new data point, look at its <em>k</em> nearest neighbors in the training data and take a majority vote.</p>
<p>No training phase. No model to fit. No assumptions about the data. Just store everything and compute distances when you need a prediction. It’s the algorithm equivalent of “when in Rome, do as the Romans do.”</p>
</section>
<section id="the-algorithm-4-steps" class="level3">
<h3 class="anchored" data-anchor-id="the-algorithm-4-steps">The Algorithm (4 Steps)</h3>
<ol type="1">
<li>Pick a value of <em>k</em> (e.g.&nbsp;3, 5, 7)</li>
<li>Compute the distance from the query point to <strong>every</strong> training point</li>
<li>Find the <em>k</em> closest ones</li>
<li>Return the majority class among those <em>k</em> neighbors</li>
</ol>
</section>
<section id="building-knn-from-scratch" class="level3">
<h3 class="anchored" data-anchor-id="building-knn-from-scratch">Building KNN from Scratch</h3>
<p>Let’s build it step by step — like assembling IKEA furniture, but with fewer leftover screws. First, we need a way to measure distance between two points — the <strong>Euclidean distance</strong> (straight-line distance from geometry class):</p>
<div id="cbb2d74f" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb1-2"></span>
<span id="cb1-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> dist(x, y, axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>):</span>
<span id="cb1-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">isinstance</span>(x, np.ndarray): x, y <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array(x), np.array(y)</span>
<span id="cb1-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.sqrt(((x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> y) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>axis, keepdims<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>))</span>
<span id="cb1-6"></span>
<span id="cb1-7">dist([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]), dist([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>])</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="101">
<pre><code>(array([0.]), array([1.]))</code></pre>
</div>
</div>
<p>Now the KNN classifier itself — compute distances, find the <em>k</em> nearest, take a majority vote:</p>
<div id="a281e4c0" class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1">X_train <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>]])</span>
<span id="cb3-2">y_train <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># two clusters: class 0 and class 1</span></span>
<span id="cb3-3">x_query <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">2.5</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">3.0</span>])</span>
<span id="cb3-4">k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span></span>
<span id="cb3-5"></span>
<span id="cb3-6"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> knn_predict(X_train, y_train, x_query, k):</span>
<span id="cb3-7">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Classify x_query using k nearest neighbors."""</span></span>
<span id="cb3-8">    dists <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dist(x_query, X_train, axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>).squeeze()</span>
<span id="cb3-9">    top_k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dists.argsort()[:k]</span>
<span id="cb3-10">    labels <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> y_train[top_k]</span>
<span id="cb3-11">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.bincount(labels).argmax()</span>
<span id="cb3-12"></span>
<span id="cb3-13">knn_predict(X_train, y_train, x_query, k<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-display" data-execution_count="80">
<pre><code>np.int64(0)</code></pre>
</div>
</div>
<p><strong>Result:</strong> Class <code>0</code> ✅ — makes sense, since <code>[2.5, 3.0]</code> is geometrically close to the class 0 cluster.</p>
<p><strong>Caveat:</strong> <code>np.bincount</code> requires non-negative integer labels (0, 1, 2, …). For arbitrary labels, use <code>collections.Counter</code> for the majority vote instead.</p>
</section>
<section id="seeing-knn-in-action" class="level3">
<h3 class="anchored" data-anchor-id="seeing-knn-in-action">Seeing KNN in Action</h3>
<p>Words are nice, but a picture is worth a thousand distances. Let’s plot the two clusters, a query point sitting between them, and draw lines to its <em>k</em> nearest neighbors:</p>
<div id="57e7c76f" class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> matplotlib.pyplot <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> plt</span>
<span id="cb5-2"></span>
<span id="cb5-3">X_train <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>],[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>]])</span>
<span id="cb5-4">y_train <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])</span>
<span id="cb5-5">x_query <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>])</span>
<span id="cb5-6">k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span></span>
<span id="cb5-7"></span>
<span id="cb5-8">dists <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.sqrt(((X_train <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> x_query)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>))</span>
<span id="cb5-9">top_k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dists.argsort()[:k]</span>
<span id="cb5-10"></span>
<span id="cb5-11">fig, ax <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> plt.subplots(figsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>))</span>
<span id="cb5-12">colors <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'blue'</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'red'</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> y_train]</span>
<span id="cb5-13">ax.scatter(X_train[:,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], X_train[:,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], c<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>colors, s<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>, edgecolors<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'k'</span>, zorder<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>)</span>
<span id="cb5-14"></span>
<span id="cb5-15"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> top_k:</span>
<span id="cb5-16">    ax.plot([x_query[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], X_train[i,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]], [x_query[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], X_train[i,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]], <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'g--'</span>, lw<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.7</span>)</span>
<span id="cb5-17">    ax.annotate(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'd=</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>dists[i]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.1f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span>, xy<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(X_train[i,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.15</span>, X_train[i,<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.15</span>), fontsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">9</span>)</span>
<span id="cb5-18"></span>
<span id="cb5-19">ax.scatter(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>x_query, c<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'green'</span>, s<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">200</span>, marker<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'*'</span>, edgecolors<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'k'</span>, zorder<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)</span>
<span id="cb5-20">ax.scatter([], [], c<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'blue'</span>, s<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>, edgecolors<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'k'</span>, label<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Class 0'</span>)</span>
<span id="cb5-21">ax.scatter([], [], c<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'red'</span>, s<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>, edgecolors<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'k'</span>, label<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'Class 1'</span>)</span>
<span id="cb5-22">ax.scatter([], [], c<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'green'</span>, s<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>, marker<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'*'</span>, edgecolors<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'k'</span>, label<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'k=</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>k<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> neighbors'</span>)</span>
<span id="cb5-23">ax.set_title(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'KNN Classification (k=</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>k<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">)'</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> ax.legend()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> ax.grid(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>, alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.3</span>)</span>
<span id="cb5-24">plt.tight_layout()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> plt.show()</span></code></pre></div></div>
</details>
<div class="cell-output cell-output-display">
<div>
<figure class="figure">
<p><img src="https://risheekkumar.in/posts/KNN/KNN_files/figure-html/cell-4-output-1.png" class="img-fluid figure-img"></p>
</figure>
</div>
</div>
</div>
</section>
<section id="choosing-k-the-bias-variance-tradeoff" class="level3">
<h3 class="anchored" data-anchor-id="choosing-k-the-bias-variance-tradeoff">Choosing k — The Bias-Variance Tradeoff</h3>
<p>Imagine you’re on a game show. You need to guess the price of a car. You can ask 1, 5, or 50 audience members.</p>
<ul>
<li><strong>Ask just 1 person (k=1):</strong> If they’re a car mechanic, great! If they think cars cost $50, you’re toast. Wildly sensitive to who you pick — <strong>high variance, low bias</strong>.</li>
<li><strong>Ask 50 people (k=50):</strong> The average will be safe but won’t reflect the one expert in the crowd. You’ve smoothed away useful signal — <strong>low variance, high bias</strong>.</li>
<li><strong>Ask 5 people (k=5):</strong> A sweet spot. Enough voices to cancel out the outlier, few enough to keep the experts’ opinions.</li>
</ul>
<p>In general, <em>k</em> controls how smooth or noisy your predictions are:</p>
<ul>
<li><strong>Small k</strong> (e.g.&nbsp;k=1): the model memorizes training data — <strong>high variance, low bias</strong> (overfitting)</li>
<li><strong>Large k</strong> (approaching N): the model ignores local structure — <strong>low variance, high bias</strong> (underfitting)</li>
<li><strong>Odd k</strong> is preferred for binary classification to avoid ties</li>
</ul>
<p>In practice, you don’t guess <em>k</em>. You use <strong>cross-validation</strong>: train KNN with k=1, 3, 5, 7,…, evaluate each on a validation set, and pick the one with the best accuracy.</p>
</section>
<section id="knn-for-regression" class="level3">
<h3 class="anchored" data-anchor-id="knn-for-regression">KNN for Regression</h3>
<p>KNN isn’t just for classification. For <strong>regression</strong>, instead of taking a majority vote, you take the <strong>mean</strong> of the <em>k</em> neighbors’ target values. Think of it like estimating the temperature: you look at the <em>k</em> closest weather stations and average their readings.</p>
<div id="d6b85080" class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> knn_predict_regression(X_train, y_train, x_query, k):</span>
<span id="cb6-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Predict continuous value using k nearest neighbors."""</span></span>
<span id="cb6-3">    dists <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dist(x_query, X_train, axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>).squeeze()</span>
<span id="cb6-4">    top_k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dists.argsort()[:k]</span>
<span id="cb6-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> y_train[top_k].mean()</span></code></pre></div></div>
</details>
</div>
</section>
<section id="weighted-knn" class="level3">
<h3 class="anchored" data-anchor-id="weighted-knn">Weighted KNN</h3>
<p>In standard KNN, all <em>k</em> neighbors vote equally — democracy! But sometimes your closest neighbor’s opinion should matter more than one 10 streets away. In <strong>weighted KNN</strong>, closer neighbors get more influence — typically using inverse distance as weight. It’s the difference between asking 5 random coworkers for lunch advice vs.&nbsp;asking the one sitting right next to you <em>plus</em> 4 others.</p>
<div id="4366c16d" class="cell">
<details class="code-fold">
<summary>Code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> knn_predict_weighted(X_train, y_train, x_query, k):</span>
<span id="cb7-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Weighted KNN: closer neighbors get more vote power."""</span></span>
<span id="cb7-3">    dists <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dist(x_query, X_train, axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>).squeeze()</span>
<span id="cb7-4">    top_k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> dists.argsort()[:k]</span>
<span id="cb7-5">    weights <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (dists[top_k] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1e-8</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># avoid division by zero</span></span>
<span id="cb7-6">    labels <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> y_train[top_k]</span>
<span id="cb7-7">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Weighted vote for each class</span></span>
<span id="cb7-8">    classes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.unique(y_train)</span>
<span id="cb7-9">    scores <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {c: weights[labels <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> c].<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> classes}</span>
<span id="cb7-10">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">max</span>(scores, key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>scores.get)</span></code></pre></div></div>
</details>
</div>
</section>
<section id="decision-boundaries" class="level3">
<h3 class="anchored" data-anchor-id="decision-boundaries">Decision Boundaries</h3>
<p>KNN produces <strong>piecewise linear</strong> decision boundaries — think of them like country borders drawn by neighbors voting on which side of the line they belong to. As <em>k</em> decreases, the borders get jagged and weird (overfitting — like a gerrymandered district). As <em>k</em> increases, borders smooth out into clean curves (underfitting — like someone drew them with a ruler). Sketch this in your mind — remember: small <em>k</em> = squiggly, large <em>k</em> = smooth.</p>
</section>
<section id="feature-scaling" class="level3">
<h3 class="anchored" data-anchor-id="feature-scaling">Feature Scaling</h3>
<p>You <strong>must</strong> normalize features before using KNN. Here’s a cautionary tale:</p>
<p>Suppose you’re matching people by similarity for a roommate app.</p>
<ul>
<li>Feature 1: <strong>annual income</strong> ($20k–$200k).</li>
<li>Feature 2: <strong>cleanliness rating</strong> (1–10).</li>
</ul>
<p>Without scaling, income differences of $10,000+ completely drown out cleanliness differences of a few points. KNN will pair you with people who earn the same as you — even if they never wash the dishes.</p>
<p>Standard fix: <strong>standardize</strong> each feature to zero mean and unit variance, or <strong>min-max scale</strong> to [0, 1]. Now both features contribute equally, and you get a roommate who matches you on <em>both</em> axes.</p>
</section>
<section id="curse-of-dimensionality" class="level3">
<h3 class="anchored" data-anchor-id="curse-of-dimensionality">Curse of Dimensionality</h3>
<p>Picture this: you’re in a dark room looking for your keys. In 1D (a hallway), you just walk forward — easy search. In 2D (a room), it’s harder but manageable. In 3D (a building), tougher. Now imagine a 100D building — every direction you look, the space stretches out forever, and your keys might as well be nowhere. Worse, <em>every pair of keys looks roughly the same distance away</em>.</p>
<p>That’s the curse of dimensionality. As dimensions increase, the volume of the space explodes exponentially. A unit hypersphere in high dimensions has essentially zero volume relative to the enclosing cube — data points end up near corners/edges, and pairwise distances become nearly equal.</p>
<p><strong>The result:</strong> “nearest” neighbor becomes nearly meaningless in high dimensions.</p>
<p><strong>Common misconception:</strong> Changing the distance metric (e.g.&nbsp;to cosine similarity) fixes this. It doesn’t — even angles concentrate around 90° in high dimensions.</p>
<p><strong>The actual fix:</strong> Reduce dimensionality first (PCA or pretrained embeddings), then apply KNN.</p>
<ul>
<li><strong>PCA</strong> — unsupervised, linear, no extra training needed</li>
<li><strong>Embeddings</strong> (e.g.&nbsp;Word2Vec, neural) — pretrained on large external data, captures non-linear structure</li>
</ul>
</section>
<section id="distance-metrics" class="level3">
<h3 class="anchored" data-anchor-id="distance-metrics">Distance Metrics</h3>
<p>Different situations call for different notions of “closeness”:</p>
<ul>
<li><strong>Euclidean</strong> — “as the crow flies”; straight-line distance, works well in low dimensions</li>
<li><strong>Cosine similarity</strong> — “same direction, who cares how far”; measures angle, not magnitude; good for sparse/high-dimensional text data</li>
<li><strong>Manhattan (L1)</strong> — “as the taxi drives” (block by block); sum of absolute differences; more robust to outliers</li>
</ul>
</section>
<section id="time-space-complexity" class="level3">
<h3 class="anchored" data-anchor-id="time-space-complexity">Time &amp; Space Complexity</h3>
<table class="caption-top table">
<thead>
<tr class="header">
<th></th>
<th>KNN</th>
<th>Most ML algorithms</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Training</strong></td>
<td>O(1) — just store data</td>
<td>O(expensive)</td>
</tr>
<tr class="even">
<td><strong>Prediction</strong></td>
<td>O(Nd) — expensive</td>
<td>O(1) — just apply model</td>
</tr>
</tbody>
</table>
<p>This is the <strong>reverse</strong> of most ML algorithms. KNN is “lazy” — it shows up to the exam having done zero prep, then frantically flips through the textbook during the test. This matters when you have large datasets or need real-time predictions — you can’t afford to flip through a million pages every time someone asks a question.</p>
<p><strong>Scalability fixes:</strong> - <strong>KD-trees / Ball trees</strong> — spatial indexing structures, fast for low dimensions (&lt; ~20) - <strong>Approximate Nearest Neighbor (ANN)</strong> — e.g.&nbsp;FAISS, HNSW; trades exact results for massive speedups</p>
</section>
<section id="when-not-to-use-knn" class="level3">
<h3 class="anchored" data-anchor-id="when-not-to-use-knn">When NOT to Use KNN</h3>
<p>KNN is a great hammer, but not everything is a nail. Steer clear when:</p>
<ul>
<li><strong>High dimensions</strong> without dimensionality reduction — the curse turns “nearest neighbor” into “random neighbor”</li>
<li><strong>Very large datasets</strong> — asking O(Nd) questions at prediction time gets expensive fast; consider ANN</li>
<li><strong>When you need interpretability</strong> — “because those 5 points over there said so” isn’t a satisfying answer to a stakeholder</li>
<li><strong>Imbalanced classes</strong> — if 95% of your data is class A, a majority vote with any reasonable <em>k</em> just predicts A every time</li>
</ul>


</section>

 ]]></description>
  <category>code</category>
  <category>research paper</category>
  <category>impact</category>
  <guid>https://risheekkumar.in/posts/KNN/KNN.html</guid>
  <pubDate>Thu, 14 Mar 2024 18:30:00 GMT</pubDate>
  <media:content url="https://risheekkumar.in/posts/KNN/knn.webp" medium="image" type="image/webp"/>
</item>
</channel>
</rss>
