Inside the SNN: LIF Neurons and SuperSpike Learning

Lesson 2 of N — SNNBot_garage/src/SNNBot.nim

In Lesson 1, you saw the architecture: 80 input neurons → 12 hidden LIF neurons → 2 output channels (sin/cos). This lesson breaks open each piece — how neurons fire, how inputs are encoded, how the network learns. Every code snippet is from YOUR implementation in SNNBot.nim.

1. The Leaky Integrate-and-Fire Neuron

The LIF model as implemented:

V[t] = LEAK × V[t-1] + weighted_input
if V >= THRESH: spike! V = 0

Key constants from the code:

With 80 inputs and weights initialized at ±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 — most neurons fire most ticks.

The exact forward pass code for the hidden layer:

for h in 0 ..< N_HID:
    var wsum = 0.0
    for i in 0 ..< 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
The LIF neuron is just a leaky accumulator with a threshold. It is the simplest spiking neuron — one step above a perceptron. The "leak" gives it temporal memory: recent inputs matter more than old ones.

2. Input Encoding — Triangular Interpolation

Continuous values (bearing, velocity direction, speed) must become spike patterns. The bearing encoder uses 36 neurons covering 360°, each neuron representing a 10° band:

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

Triangular interpolation in action:

bearing = 25°
band 2 (20°): activation = 0.5  ▓▓▓▓▓░░░░░
band 3 (30°): activation = 0.5  ▓▓▓▓▓░░░░░
all others:   activation = 0.0  ░░░░░░░░░░
This is population coding — 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.

The 80-neuron layout:

Notice: bearing and velocity direction both use ABSOLUTE angles. The previous session discovered that using RELATIVE bearing caused a feedback loop — aiming changed the input, which changed the aim. Absolute angles break this loop.

3. The Output — Polar Coding

The output layer sums hidden spikes weighted by learned coefficients:

sinOut = 0.0; cosOut = 0.0
for h in 0 ..< N_HID:
    sinOut += spikesOut[h] * snn.wSin[h]
    cosOut += spikesOut[h] * snn.wCos[h]

Then decoded: angle = atan2(sinOut, cosOut) × 180/π

Why sin/cos instead of outputting an angle directly? Because angles wrap — 359° and 1° 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.

N_INFER = 10: the network runs 10 ticks on the SAME input, accumulating sin/cos outputs. This averaging stabilizes the output — a single tick's spikes are noisy (binary), but the average over 10 ticks is smooth.

4. The SuperSpike Learning Rule

4a. The Problem: Spikes Aren't Differentiable

A spike is binary: 0 or 1. You can't take the gradient of a step function — it's zero everywhere except at the threshold, where it's infinity. So backpropagation doesn't work directly.

4b. The Surrogate Gradient

SuperSpike replaces the true derivative with a smooth surrogate:

proc surrogateDerivative(v: float): float =
  let x = BETA * (v - THRESH)
  result = 1.0 / ((1.0 + abs(x)) * (1.0 + abs(x)))

This is a bell curve centered at v = THRESH. 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).

σ'(v) 1.0 | ∧ | / \ 0.5 | / \ | / \ 0.0 |----/---------\---- 0 THRESH 2×THRESH v

4c. The Three-Factor Rule

Each weight update is the product of THREE factors.

For hidden→output weights (wSin, wCos):

Δw = η × rate_h × σ'(V_h) × error

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 (σ'≈0) doesn't learn. If the output is correct (error=0), nothing learns.

For input→hidden weights (wih):

Δw = η_ih × preTrace_i × σ'(V_h) × error_h

4d. Random Feedback Alignment

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:

let errHid = snn.bFb[h * 2 + 0] * errSin + snn.bFb[h * 2 + 1] * errCos

These bFb weights are initialized randomly and NEVER updated. This is called feedback alignment — a controversial but effective shortcut. The hidden layer learns to align its representation with these random projections.

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 WT. It works because the forward weights W gradually align with B during training.

5. Weight Initialization

proc initSNN(snn: var SNN) =
  for w in snn.wih.mitems:  w = rand(0.2) - 0.1   # ±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   # ±1.0, fixed forever
Weights start small (±0.1) with room to grow to ±1.0 (W_CLAMP). Feedback weights are larger (±1.0) because they need to project meaningful error signals.

6. Why Is the SNN Dormant?

The honest answer:

Active bug: The SNN path currently sets targetAngle = gunDir + snnAngle (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.

7. Check Your Understanding

Q1: What does the surrogate derivative do?

Correct. The spike function is a step — 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.
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.

Q2: In the three-factor rule Δw = η × rate × σ'(V) × error, what happens when a neuron's voltage is far from threshold?

Correct. The surrogate derivative σ'(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 — only neurons near the decision boundary learn.
Not quite. σ'(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.

Q3: Why does the SNN use random feedback weights (bFb) instead of transposing the output weights?

Correct. Transposing W for backprop requires passing gradients through the spike function — 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.
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.

Q4: What is the feedback loop bug in the SNN path?

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.
Not quite. The bug is the relative output: targetAngle = gunDir + snnAngle. As the gun turns, gunDir changes, which changes the bearing inputs, which changes snnAngle. The output feeds back into the input, creating instability.

8. Next Steps

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 — your first live SNN training run.

Questions? Ask your agent. This is complex material — re-read sections 4a–4d until the three-factor rule clicks.