Files
SirRoboGarage/lessons/0002-snn-internals-lif-and-superspike.html

264 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lesson 2: Inside the SNN — LIF Neurons and SuperSpike Learning</title>
<link rel="stylesheet" href="../assets/style.css">
</head>
<body>
<h1>Inside the SNN: LIF Neurons and SuperSpike Learning</h1>
<p style="font-family: var(--font-sans); font-size: 0.85rem; color: #888;">Lesson 2 of N &mdash; <code>SNNBot_garage/src/SNNBot.nim</code></p>
<p>In Lesson 1, you saw the architecture: 80 input neurons &rarr; 12 hidden LIF neurons &rarr; 2 output channels (sin/cos). This lesson breaks open each piece &mdash; how neurons fire, how inputs are encoded, how the network learns. Every code snippet is from YOUR implementation in SNNBot.nim.</p>
<!-- ═══════════════════════════════════════════════════════════════════════ -->
<h2>1. The Leaky Integrate-and-Fire Neuron</h2>
<p>The LIF model as implemented:</p>
<pre><code>V[t] = LEAK × V[t-1] + weighted_input
if V >= THRESH: spike! V = 0</code></pre>
<p>Key constants from the code:</p>
<ul>
<li><code>LEAK = 0.9</code> &mdash; membrane leaks 10% per tick &mdash; neuron "forgets" over ~10 ticks</li>
<li><code>THRESH = 0.08</code> &mdash; very low threshold &mdash; easy to fire</li>
</ul>
<div class="note">
With 80 inputs and weights initialized at &plusmn;0.1, the expected input sum is ~0. The low threshold (0.08) means even small positive fluctuations cause spikes. This makes the hidden layer very active early on &mdash; most neurons fire most ticks.
</div>
<p>The exact forward pass code for the hidden layer:</p>
<pre><code>for h in 0 ..&lt; N_HID:
var wsum = 0.0
for i in 0 ..&lt; N_IN:
wsum += inputs[i] * snn.wih[i * N_HID + h]
snn.vHid[h] = LEAK * snn.vHid[h] + wsum
if snn.vHid[h] >= THRESH:
spikesOut[h] = 1.0
snn.vHid[h] = 0.0 # reset
else:
spikesOut[h] = 0.0</code></pre>
<div class="key-insight">
The LIF neuron is just a leaky accumulator with a threshold. It is the simplest spiking neuron &mdash; one step above a perceptron. The "leak" gives it temporal memory: recent inputs matter more than old ones.
</div>
<!-- ═══════════════════════════════════════════════════════════════════════ -->
<h2>2. Input Encoding &mdash; Triangular Interpolation</h2>
<p>Continuous values (bearing, velocity direction, speed) must become spike patterns. The bearing encoder uses 36 neurons covering 360&deg;, each neuron representing a 10&deg; band:</p>
<pre><code>proc encodeBearing(inputs: var array[N_IN, float], bearing: float, offset: int) =
let norm = ((bearing + 180.0) / BAND_DEG) # 0..36
let lo = int(norm) mod 36
let hi = (lo + 1) mod 36
let frac = norm - float(int(norm))
inputs[offset + lo] = 1.0 - frac # stronger for closer band
inputs[offset + hi] = frac # weaker for farther band</code></pre>
<p>Triangular interpolation in action:</p>
<pre><code>bearing = 25°
band 2 (20°): activation = 0.5 ▓▓▓▓▓░░░░░
band 3 (30°): activation = 0.5 ▓▓▓▓▓░░░░░
all others: activation = 0.0 ░░░░░░░░░░</code></pre>
<div class="note">
This is population coding &mdash; the same trick the brain uses for direction. No single neuron says "25 degrees"; the ratio between two adjacent neurons encodes it. Smooth interpolation means similar angles activate similar patterns.
</div>
<p>The 80-neuron layout:</p>
<ul>
<li>Neurons 0&ndash;35: relative bearing (36 neurons, 10&deg;/band, wraps circularly)</li>
<li>Neurons 36&ndash;71: velocity direction (36 neurons, 10&deg;/band, wraps circularly)</li>
<li>Neurons 72&ndash;79: speed (8 neurons, 1 unit/tick per band, clamped at 8)</li>
</ul>
<div class="warning">
<strong>Notice:</strong> bearing and velocity direction both use ABSOLUTE angles. The previous session discovered that using RELATIVE bearing caused a feedback loop &mdash; aiming changed the input, which changed the aim. Absolute angles break this loop.
</div>
<!-- ═══════════════════════════════════════════════════════════════════════ -->
<h2>3. The Output &mdash; Polar Coding</h2>
<p>The output layer sums hidden spikes weighted by learned coefficients:</p>
<pre><code>sinOut = 0.0; cosOut = 0.0
for h in 0 ..&lt; N_HID:
sinOut += spikesOut[h] * snn.wSin[h]
cosOut += spikesOut[h] * snn.wCos[h]</code></pre>
<p>Then decoded: <code>angle = atan2(sinOut, cosOut) &times; 180/&pi;</code></p>
<div class="key-insight">
Why sin/cos instead of outputting an angle directly? Because angles wrap &mdash; 359&deg; and 1&deg; are close, but numerically far apart. Sin/cos is the standard trick: the network outputs a point on the unit circle, and atan2 recovers the angle. No wrapping discontinuity.
</div>
<p><code>N_INFER = 10</code>: the network runs 10 ticks on the SAME input, accumulating sin/cos outputs. This averaging stabilizes the output &mdash; a single tick's spikes are noisy (binary), but the average over 10 ticks is smooth.</p>
<!-- ═══════════════════════════════════════════════════════════════════════ -->
<h2>4. The SuperSpike Learning Rule</h2>
<h3>4a. The Problem: Spikes Aren't Differentiable</h3>
<p>A spike is binary: 0 or 1. You can't take the gradient of a step function &mdash; it's zero everywhere except at the threshold, where it's infinity. So backpropagation doesn't work directly.</p>
<h3>4b. The Surrogate Gradient</h3>
<p>SuperSpike replaces the true derivative with a smooth surrogate:</p>
<pre><code>proc surrogateDerivative(v: float): float =
let x = BETA * (v - THRESH)
result = 1.0 / ((1.0 + abs(x)) * (1.0 + abs(x)))</code></pre>
<p>This is a bell curve centered at <code>v = THRESH</code>. It is large when the voltage is NEAR the threshold (the neuron almost spiked or just barely spiked), and small when far from threshold (irrelevant neurons don't learn).</p>
<div class="diagram">&sigma;'(v)
1.0 &vert; &and;
&vert; / \
0.5 &vert; / \
&vert; / \
0.0 &vert;----/---------\----
0 THRESH 2&times;THRESH v</div>
<h3>4c. The Three-Factor Rule</h3>
<p>Each weight update is the product of THREE factors.</p>
<p>For hidden&rarr;output weights (<code>wSin</code>, <code>wCos</code>):</p>
<pre><code>&Delta;w = &eta; &times; rate_h &times; &sigma;'(V_h) &times; error</code></pre>
<ul>
<li><strong>&eta; = 0.1</strong>: learning rate</li>
<li><strong>rate_h</strong>: spike rate of hidden neuron h (spikes/N_INFER) &mdash; "was this neuron active?"</li>
<li><strong>&sigma;'(V_h)</strong>: surrogate derivative &mdash; "was this neuron near threshold?"</li>
<li><strong>error</strong>: target_rate &minus; actual_rate &mdash; "how wrong was the output?"</li>
</ul>
<p>All three must be non-zero for learning to happen. A neuron that didn't fire (rate=0) doesn't learn. A neuron far from threshold (&sigma;'&asymp;0) doesn't learn. If the output is correct (error=0), nothing learns.</p>
<p>For input&rarr;hidden weights (<code>wih</code>):</p>
<pre><code>&Delta;w = &eta;_ih &times; preTrace_i &times; &sigma;'(V_h) &times; error_h</code></pre>
<ul>
<li><strong>preTrace_i</strong>: low-pass filtered input (TRACE_DECAY=0.9) &mdash; "was this input recently active?"</li>
<li><strong>error_h</strong>: hidden error, projected via random feedback weights</li>
</ul>
<h3>4d. Random Feedback Alignment</h3>
<p>How does the hidden layer know its error? In backprop, you'd use the transpose of the output weights. SuperSpike uses RANDOM FIXED weights instead:</p>
<pre><code>let errHid = snn.bFb[h * 2 + 0] * errSin + snn.bFb[h * 2 + 1] * errCos</code></pre>
<p>These <code>bFb</code> weights are initialized randomly and NEVER updated. This is called feedback alignment &mdash; a controversial but effective shortcut. The hidden layer learns to align its representation with these random projections.</p>
<div class="key-insight">
This is the key advantage over backprop for spiking networks: no need to propagate gradients through the spike function. The random feedback matrix B replaces W<sup>T</sup>. It works because the forward weights W gradually align with B during training.
</div>
<!-- ═══════════════════════════════════════════════════════════════════════ -->
<h2>5. Weight Initialization</h2>
<pre><code>proc initSNN(snn: var SNN) =
for w in snn.wih.mitems: w = rand(0.2) - 0.1 # &plusmn;0.1
for w in snn.wSin.mitems: w = rand(0.2) - 0.1
for w in snn.wCos.mitems: w = rand(0.2) - 0.1
for b in snn.bFb.mitems: b = rand(2.0) - 1.0 # &plusmn;1.0, fixed forever</code></pre>
<div class="note">
Weights start small (&plusmn;0.1) with room to grow to &plusmn;1.0 (W_CLAMP). Feedback weights are larger (&plusmn;1.0) because they need to project meaningful error signals.
</div>
<!-- ═══════════════════════════════════════════════════════════════════════ -->
<h2>6. Why Is the SNN Dormant?</h2>
<p>The honest answer:</p>
<ul>
<li>The grid accumulator was introduced as a simpler baseline to debug the aiming pipeline</li>
<li>The grid reached 48% hit rate &mdash; proving the pipeline works (state machine, bearing calc, fire gate)</li>
<li>The SNN was never tuned against the corrected pipeline (frozen DECIDE snapshot, offset-based learning)</li>
<li>Key open questions: Does the SNN converge? How many rounds to learn? Is 12 hidden neurons enough?</li>
</ul>
<div class="warning">
<strong>Active bug:</strong> The SNN path currently sets <code>targetAngle = gunDir + snnAngle</code> (RELATIVE to gun), not absolute bearing. This is the feedback loop bug that was fixed for the grid path. The SNN path still has this bug.
</div>
<!-- ═══════════════════════════════════════════════════════════════════════ -->
<h2>7. Check Your Understanding</h2>
<div class="quiz" id="q1">
<h3>Q1: What does the surrogate derivative do?</h3>
<label><input type="radio" name="q1" data-correct="true"> Approximates the gradient of the spike function for learning</label>
<label><input type="radio" name="q1" data-correct="false"> Smooths the membrane voltage to prevent oscillation</label>
<label><input type="radio" name="q1" data-correct="false"> Decays the membrane potential over time</label>
<label><input type="radio" name="q1" data-correct="false"> Normalizes the spike rate across neurons</label>
<div class="feedback correct">Correct. The spike function is a step &mdash; zero gradient everywhere except the threshold. The surrogate replaces it with a smooth bell curve so that gradient-based updates can flow through. It is a deliberate approximation, not a description of what the neuron physically does.</div>
<div class="feedback wrong">Not quite. The surrogate derivative exists specifically to give a usable gradient signal through the non-differentiable spike threshold, enabling weight updates that would otherwise be impossible.</div>
</div>
<div class="quiz" id="q2">
<h3>Q2: In the three-factor rule &Delta;w = &eta; &times; rate &times; &sigma;'(V) &times; error, what happens when a neuron's voltage is far from threshold?</h3>
<label><input type="radio" name="q2" data-correct="false"> The weight update is large because the neuron needs to change</label>
<label><input type="radio" name="q2" data-correct="true"> The weight update is zero because &sigma;'(V) &asymp; 0</label>
<label><input type="radio" name="q2" data-correct="false"> The neuron fires more frequently</label>
<label><input type="radio" name="q2" data-correct="false"> The learning rate &eta; is adjusted automatically</label>
<div class="feedback correct">Correct. The surrogate derivative &sigma;'(V) peaks at threshold and drops toward zero for voltages far from it. A neuron that is either deeply sub-threshold or has just reset contributes almost nothing to the weight update &mdash; only neurons near the decision boundary learn.</div>
<div class="feedback wrong">Not quite. &sigma;'(V) is the bell curve centered at THRESH. Far from threshold it is near zero, which multiplies the whole update to near zero. The neuron is effectively excluded from learning that tick.</div>
</div>
<div class="quiz" id="q3">
<h3>Q3: Why does the SNN use random feedback weights (<code>bFb</code>) instead of transposing the output weights?</h3>
<label><input type="radio" name="q3" data-correct="false"> Random weights are faster to compute</label>
<label><input type="radio" name="q3" data-correct="true"> It avoids propagating gradients through non-differentiable spikes</label>
<label><input type="radio" name="q3" data-correct="false"> Random weights provide better generalization</label>
<label><input type="radio" name="q3" data-correct="false"> The output weights are too small to transpose</label>
<div class="feedback correct">Correct. Transposing W for backprop requires passing gradients through the spike function &mdash; which has no useful gradient. Random fixed feedback weights sidestep this entirely. The forward weights gradually align with B during training (feedback alignment), so the signal is noisy but directionally correct.</div>
<div class="feedback wrong">Not quite. The fundamental barrier is the spike function's non-differentiability. Transposing W doesn't help if you can't propagate a gradient through the spike. Random feedback avoids this by not needing gradients through spikes at all.</div>
</div>
<div class="quiz" id="q4">
<h3>Q4: What is the feedback loop bug in the SNN path?</h3>
<label><input type="radio" name="q4" data-correct="false"> The SNN uses too few hidden neurons</label>
<label><input type="radio" name="q4" data-correct="true"> Output is relative to gun direction, so aiming changes the input which changes the aim</label>
<label><input type="radio" name="q4" data-correct="false"> The learning rate is too high</label>
<label><input type="radio" name="q4" data-correct="false"> The surrogate derivative is centered at the wrong threshold</label>
<div class="feedback correct">Correct. When targetAngle is computed as gunDir + snnAngle, the gun turns toward that target. But the input encoding includes the current gun direction, so as the gun moves, the inputs change, which changes snnAngle, which changes where the gun moves. The system chases its own tail. The fix: use absolute angles in both input and output.</div>
<div class="feedback wrong">Not quite. The bug is the relative output: <code>targetAngle = gunDir + snnAngle</code>. As the gun turns, gunDir changes, which changes the bearing inputs, which changes snnAngle. The output feeds back into the input, creating instability.</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════ -->
<h2>8. Next Steps</h2>
<p>You now understand both paths in your bot. The grid works but can't scale. The SNN can learn but hasn't been tested with the pipeline fixes. In the next lesson, we'll activate the SNN, fix the feedback loop bug, and run it against Walls &mdash; your first live SNN training run.</p>
<div class="note">
Questions? Ask your agent. This is complex material &mdash; re-read sections 4a&ndash;4d until the three-factor rule clicks.
</div>
<div class="nav-footer">
Lesson 2 / LIF Neurons and SuperSpike &mdash;
source: <code>SNNBot_garage/src/SNNBot.nim</code> &mdash;
<a href="0001-snnbot-architecture-overview.html">&larr; Lesson 1: Architecture Overview</a>
</div>
<script src="../assets/quiz.js"></script>
</body>
</html>