Using A Kalman Filter for Neural Decoding
planted aug 18, 2026 · v1.0.0
The task is to decode the movement (x and y position + x and y velocity) of a monkey moving a cursor based on spiking activities of some neurons.
The principle we follow is that precision adds. When we have two independent and noisy measurements of a truth, we can add them with a certain weight and end up with a measurement that is more precise than both raw measurements. This is called a Kalman filter, and it looks mathematically identical to the EMA that I used to do velocity smoothing for a noisy velocity measurement in the inverted pendulum bot.
Let’s try to prove that has a higher precision than both and - i.e. the variance of is lower than both of them.
Let be (Since both are noisy measurements around the same ground truth, no bias).
Differentiate with respect to and set to zero to minimize the variance:
What does this remind you of? E&M was only a semester ago for me so this jumps out as a parallel circuit’s resistance.
Then, naturally, as the combined resistance is lower for parallel circuits than each resistor’s, so does the variance of the blended measurement. There is a shared mechanism behind both. Circuits also minimize something - power.
And analogous to conductance, the inverses of their variance, as we mentioned, adds up to the combined precision (of ):
We have proven that precision adds when we blend two independent and noisy raw measurements with the optimal weight , their precisions add. We end up with a better measurement than both. Yet, in the case of neural decoding, it looks like we only have one raw measurement - the spikes. So here comes the cool trick:
We get to use the prediction of the previous timestep as an independent input.
This is similar to a Wiener filter, where we directly use the previous bins to inform the current one. The reason we can do this is because motion is continuous, and momentum determines a large portion of the following movement.
First Half: Momentum
Let’s assume a linear relationship between the previous state and the current state (why?).
But it is not always linear. There is noise, and the monkey changes its movement all the time. Let’s call that error that we cannot account for with variance .
and are properties of the dataset (with regard to ) and are fixed. They describe how un-linear (if you get what I mean) the true data is. Furthermore, since when we decode, we don’t have the ground truth but only the previous prediction . That premise could be wrong too. At each timestep, the prediction we make has a varying error, let’s call its variance . It evolves over time by:
comes from the un-linear noise. and is how the previous error scale the current one.
Second Half: Spikes
This is not really a half, more like an eighth. The spikes is more straightforward than momentum, since there is no error carrying forward. Let’s just assume the spike give us a similarly noisy and independent measurement of the state with variance .
The Magic Blender
Now, as we proved earlier, we can now find the optimal to blend the two halves.
At this point, no one can keep track of the symbols anymore, so here is how the process goes:
- We start with nothing but the first spikes. Translate them to states. Gets , and using it as our first estimated states . .
- Moving on to the next timestep. We now have the two halves. Before we receive the second spikes, evolve the previous prediction first by our momentum guess: .
- Receiving the spikes , we now do the magical blend: .
- Go back to step 2 unless no more timestep left.
The superscript denotes step 2, the intermediate estimate and error solely from momentum, before receiving the next spikes.
The Matrix
Welcome to the real world, Neo.
In reality, we have not 1, but 4 numbers representing the state of a cursor. its x-position, y-position, x-velocity, and y-velocity. We also don’t automatically get the spike’s prediction for the states. All we get is a string of processed data that tells us in each timestep, whether a neuron spiked or not. In my dataset, there are 223 such neurons.
Let us try to map the scalar versions of our equations over.
Here, and are vectors of height 4. and is a matrix. Let be the error that cannot account for, and be its covariance - a 4 matrix.
Before we look at what covariance is. Let’s appreciate the fact that we have introduced complexity on our motion model. We used to have a single state in the scalar version - velocity. Now we have four. So each state becomes a linear combination of all the previous states: . E.g., the estimated x-position may be .
If we try to again find how the variance of their error behave, we are adding the error of the four states. If the states were independent, we can simply add their variances like we did before. However, the four states are clearly not independent. That is why we need to look at covariance.
Covariance
Recall how variance is the expected value of the squared difference between a random variable and its mean. The reason we squared it was to keep it positive, while keeping the algebra easy (unlike absolute value).
How do we compute the variance of a sum of two random variables? If they are independent, we know to just add their repective variances. What if they are not independent? Let’s expand and see.
Let be and be .
When and are independent, is . Since and are both . When they are not independent, we get this term to adjust.
Covariance is about how two random variables tend to vary together. Consider two random variables, and . Let them only take values of either or . If we sum them, there are 4 possible scenarios:
| 2 | 0 | |
| 0 | -2 |
When the covariance is high (and positive), the variables tend to take similar values (in our case, the same value), the sum of the two variables have a large variance. Otherwise, they tend to take different values, the positive and negative cancels out and we tend to have a small variance (or in this case, 0).
Heading back into our quest of finding the variance of . With the added covariance to the equation, can we still elegantly represent the uncertainty of the prediction the way did in the scalar version?
Note how the covariance of a single variable is the variance of the variable. We can absolutely use a matrix-vector multiplication to express how the variance of itself propagates through . We may then add a term , corresponding to the scalar , for the variance of the error. also gets squared through covariance operator.
Looking back at the scalar version of the Kalman filter blend, we try to do something similar for the matrices:
Yet, remember in the beginning we mentioned how we don’t automatically get the spike’s prediction for the states. We only get the spikes. The intuition would be to decode the spikes directly based on their directional preferences. While that fits our scalar version better, it introduces some complications. If we fitted the decoder in the training block of the dataset, we end up with a decoder that rely partly on the spikes, and partly with knowledge about momentum - that breaks our rules that the two measurements have to be independent. We need to instead translate the states back into spikes - encoding it.
Note that is now the spikes we observe. is the true states, and is a matrix we can fit. For each neuron, we can fit its cosine tuning curve against the training block’s data. We then can calculate the expected spikes for each direction for each neuron.
Now, last step: .
In the scalar version we had:
We already have , but what is now? It was the uncertainty in the spikes’ prediction of the state earlier. We assumed we knew it, but we don’t. We only have the spikes now. Of course, neurons have noise too. Their spikes may scatter around the expected value (Poisson distribution). Let us call the covariance matrix of each neuron’s scattering .
Let’s look at one neuron and one state for now, say y-velocity. We model a neuron as direction-selective. If a neuron is sensitive to upward movements, say it fires more spikes per unit of upward velocity, when we translate 3 unit of upward velocity to spikes of this neuron, we would expect spikes above its baseline. . Where , and are all scalars.
only varies because of the noise, since is fixed.
That all looks innocent and simple, but we have 4 states and 223 neurons, not 1 and 1. For us to add and the spikes’ uncertainty, they need to mean uncertainty in the same space. is already in the state space with 4 dimensions. We need the other uncertainty to also be in the state space. Let’s go from the ground up again. We have the observed spikes , and we want to translate it to state space and find the variance of the error. With noise in each neuron’s spikes, each neuron disagrees with each other in what the state is. Let’s try to use the good old derivative and see if we can minimize the squared error to get a good estimate instead.
The loss has to be calculated in spike space, since we do not get the true state during decoding. They also have to be weighted by the precision of each neuron .
The inside is a candidate state I am searching over - not the true state, and not the answer. It is the dial I turn. The one value of it that makes smallest is the estimate, and I write that one : the state the spikes vote for.
How absolutely ugly!
Note that everything here are scalars, this is for a single state. Let’s make it 4.
This holds for each .
This still looks complicated, but something looks dangerously close to how Axler defines a matrix. Let’s try the right hand side first.
requires us to assume is diagonal (which is probably not true, but a trade-off I am taking).
So nice.
is the state that would minimize the loss function . It is parallel to , which is the momentum half’s guess of the state.
We are one last step from getting ! We still need the covariance of as well.
This is just .
Notice is the covariance and therefore symmetric (covariance between and is equivalent to covariance between and ). Its transpose equals itself.
So incredibly nice. This matches our 1-neuron-1-state version, where we claimed . And this is the that the scalar filter simply assumed we had - we have finally built it, out of and .
We are now ready for . Let’s look at the scalar version and guess an analogy:
Can we really do this though? I want to look at the total variance after the blend and see if it follows our theme of how precision adds.
Let for clarity.
Again, and are covariances and therefore are symmetric. is too, as the sum of two symmetric matrices.
Let’s call , and invert it to get precision of instead of covariance.
Woah! What do we have here? . Precision adds! We found the analogous way to make reproduces precision adding. That closes the loop. We now have a working Kalman filter on the full dataset :)
I will add experimental results to my dataset at a later date.