Spiking Neural Networks, from LIF to surrogate gradients
How the leaky integrate-and-fire neuron turns current into spikes, why that spike is a wall for backpropagation, and how the surrogate-gradient trick lets us train deep spiking networks with ordinary autodiff.
Most of my work decodes touch from event-based tactile sensors, and the natural model for that data is a Spiking Neural Network (SNN). Unlike a conventional artificial neuron that emits a smooth real number, a spiking neuron holds an internal state, integrates its input over time, and occasionally fires a discrete spike. That temporal, all-or-nothing behaviour is exactly what makes SNNs a good match for the sparse, asynchronous streams a neuromorphic tactile skin produces — and exactly what makes them awkward to train.
This post walks the shortest useful path: the leaky integrate-and-fire neuron, the non-differentiability problem it creates, and the surrogate gradient that sidesteps it.
The leaky integrate-and-fire neuron
The leaky integrate-and-fire (LIF) neuron is the workhorse of practical SNNs. It
models the membrane potential
where
The spike itself is a threshold event. When
For simulation on a digital machine we discretise time into steps of size
where
Here is that step as a small PyTorch-style module. Nothing about the forward pass is exotic — it is a loop over time with a comparison:
import torch
import torch.nn as nn
class LIF(nn.Module):
"""One leaky integrate-and-fire layer, unrolled over time."""
def __init__(self, beta: float = 0.9, v_th: float = 1.0):
super().__init__()
self.beta = beta # membrane decay, exp(-dt / tau_m)
self.v_th = v_th # firing threshold
def forward(self, current: torch.Tensor) -> torch.Tensor:
# current: (time, batch, features)
v = torch.zeros_like(current[0])
spikes = []
for t in range(current.shape[0]):
v = self.beta * v + (1.0 - self.beta) * current[t]
spike = (v >= self.v_th).float() # Heaviside threshold
v = v - self.v_th * spike # soft reset by subtraction
spikes.append(spike)
return torch.stack(spikes)
The wall: a spike has no useful gradient
To train this with gradient descent we need
This is the central difficulty of training SNNs, and it is why for years they lagged behind conventional networks: you could define them, but you could not easily fit them to data.
The surrogate-gradient trick
The surrogate-gradient method resolves this with a small, deliberate lie. We keep the hard Heaviside threshold on the forward pass — spikes stay binary, the network behaves like a real SNN — but on the backward pass we substitute the delta with the derivative of a smooth surrogate function. A convenient choice is a fast sigmoid, whose surrogate derivative is
a smooth bump centred on the threshold whose width is set by
In PyTorch this is a custom autograd.Function with mismatched forward and
backward. The forward returns the step; the backward returns the surrogate:
class SurrogateSpike(torch.autograd.Function):
"""Heaviside forward, smooth (fast-sigmoid) gradient backward."""
alpha = 10.0
@staticmethod
def forward(ctx, v_shifted): # v_shifted = V - v_th
ctx.save_for_backward(v_shifted)
return (v_shifted >= 0).float() # hard spike, as in the real neuron
@staticmethod
def backward(ctx, grad_output):
(v_shifted,) = ctx.saved_tensors
a = SurrogateSpike.alpha
surrogate = 1.0 / (1.0 + a * v_shifted.abs()) ** 2
return grad_output * surrogate
# Swap the hard comparison in LIF.forward for:
# spike = SurrogateSpike.apply(v - self.v_th)
With that single substitution the whole network — LIF layers, linear weights, convolutions feeding them — becomes differentiable end to end. You can now train it with the same optimisers, schedulers, and mixed-precision tooling you already use for a CNN. Backpropagation-through-time handles the temporal loop; the surrogate handles the spike.
Why this matters for tactile decoding
When a neuromorphic tactile sensor slides across a surface, texture shows up as
timing — the intervals between events, not just their count. An LIF layer with
a well-chosen
A few things worth knowing before you reach for them:
- The surrogate shape is a hyperparameter. Fast sigmoid, a triangular
window, and a Gaussian all work;
(the width) matters more than the exact family. - Watch the firing rate. Too few spikes and gradients vanish; too many and the network drowns in activity. A mild rate-regularisation term keeps it sane.
- Time resolution is a real cost. Backprop-through-time stores activations
for every step, so halving
roughly doubles memory.
None of this is settled, but the LIF-plus-surrogate recipe is the dependable baseline everything else is measured against — and a good place to start reading event-based touch.