# Lecture 1A: Distributional Spike Train (Encoding) Models
In this small lesson, we will explore one of the simpler cell components
that ngc-learn has to offer -- the input encoder cell. These are particularly
useful for cases where one wants to (iteratively) process/transform sensory data
features in a certain way and, furthermore, this transform is considered part
of the neuronal system (dynamics) to be simulated. To this end, we will
study two particularly useful input encoding cells in the context of
spiking neural networks -- the Bernoulli cell, for producing Bernoulli
spike trains, and the Poisson cell, for producing Poisson spike trains.
## Bernoulli Spike Trains
Oftentimes, such as in the case of pixel-based image patterns, data is
available in continuous/real-valued form, i.e., pixel values normalized to the range of
$[0,1]$. While we could directly use them as input into a network of spiking cells --
meaning we would copy the literal data vector each step in time, much like what is done
with graded rate-cell models -- for spiking neuronal systems, it would be
biologically more realistic (and more akin to how sensory data is represented
in neuromorphic platforms) if we could first convert them to binary spike trains
themselves, especially given the fact that SNNs are technically meant to process
time-varying information. While there are many ways to encode the
data as spike trains, we start with the simplest approach in this lesson and
work with an encoding scheme known as (Bernoulli) rate encoding.
Specifically, rate encoding entails normalizing the original real-valued data vector
$\mathbf{x}$ to the range of $[0,1]$ and then treating each dimension $\mathbf{x}_i$
as the probability that a spike will occur, thus yielding (for each dimension) a rate code
with a value of $\mathbf{s}_i$. In other words, each feature drives a Bernoulli
distribution of the form where $\mathbf{s}_i \sim \mathcal{B}(n, p)$ where $n = 1$
and $p = \mathbf{x}_i$. This, over time, results in a binary spike process where the
rate of firing is dictated to be solely in proportion to a feature's value.
To rate code your data, let's start by using a simple function in ngc-learn's
[Bernoulli cell](ngclearn.components.input_encoders.bernoulliCell) component.
Assuming we have a simple $10$-dimensional data vector $\mathbf{x}$ (of
shape `1 x 10`) with values in the range of $[0,1]$, we can convert it to a
spike train over $100$ steps in time as follows:
```python
from jax import numpy as jnp, random, jit
from ngclearn import Context, MethodProcess
from ngclearn.utils.viz.raster import create_raster_plot
## import model-specific mechanisms
from ngclearn.components.input_encoders.bernoulliCell import BernoulliCell
## create seeding keys (JAX-style)
dkey = random.PRNGKey(1234)
dkey, *subkeys = random.split(dkey, 2)
dt = 1. # ms # integration time constant
T = 100 ## number time steps to simulate
with Context("Model") as model:
cell = BernoulliCell("z0", n_units=10, key=subkeys[0])
advance_process = (MethodProcess("advance_proc")
>> cell.advance_state)
reset_process = (MethodProcess("reset_proc")
>> cell.reset)
def clamp(x):
cell.inputs.set(x)
probs = jnp.asarray([[0.8, 0.2, 0., 0.55, 0.9, 0, 0.15, 0., 0.6, 0.77]], dtype=jnp.float32)
spikes = []
reset_process.run()
for ts in range(T):
clamp(probs)
advance_process.run(t=ts * 1., dt=dt)
s_t = cell.outputs.get()
spikes.append(s_t)
spikes = jnp.concatenate(spikes, axis=0)
create_raster_plot(spikes, plot_fname="input_cell_raster.jpg")
```
where we notice that in the first dimension `[0,0]`, fifth dimension `[0,4]`,
and the final dimension `[0,9]` set to fairly high spike probabilities. This
code will produce and save locally to disk the following raster plot for
visualizing the resulting spike trains:
where we see that the first, middle/fifth, and tenth dimensions do indeed
result in denser spike trains. A raster plots is a simple visualization tool in
computational neuroscience for examining the trial-by-trial variability of
neural responses, allowing us to graphically examine timing (or the frequency
of firing), one of the most important aspects of neuronal action potentials/spiking
patterns. Crucially notice that the function ngc-learn offers for converting
to spike trains does so on-the-fly, meaning that you can generate binary
spike pattern vectors from a particular normalized real-valued vector whenever you
need to (this facilitates online learning setups quite well).
## Poisson Spike Trains
While the Bernoulli cell above can go a long way to providing you with useful
input spike trains, there will be some instances, experimentally, where you
might want to control the firing rate of the neurons a little bit more. This
is where the [Poisson cell](ngclearn.components.input_encoders.poissonCell)
comes into play.
For instance, with a database such as MNIST, it is often desired that the input
firing rates (of the input encoding neurons you are trying to simulate) are
within the approximate range of $0$ to $63.75$ Hertz (Hz) (as in [2]).
To do this in ngc-learn, the Poisson cell is used instead, by modifying the
header of the code above to have the following import statement:
```python
from ngclearn.components.input_encoders.poissonCell import PoissonCell
```
and by replacing the line that has the `BernoulliCell` call with the
following line instead:
```python
cell = PoissonCell("z0", n_units=10, target_freq=63.75, key=subkeys[0])
```
Running the code with the two above small modifications will
simulate a Poisson cell instead of a Bernoulli one, under the same
raw input probabilities. This should yield a plot like the one below:
The Poisson cell effectively ensures that, within the general time-scale
of ngc-learn's integration over time (milliseconds), the spike trains
iteratively produced over time will be approximately Poisson spike trains with
a maximum frequency `max_freq`.
To check that the Poisson rate approximately yields a frequency of `64` Hertz,
you could adapt the above model creation code to create a single Poisson cell
(set the `n_units = 1`) and then write the following bit of code to estimate
what the firing rate of the Poisson cell model is over a period of
`1000` milliseconds like so:
```python
dt = 1. # ms
T = 1000 ## T * dt = 1000 ms
n_trials = 30
mu = 0.
probs = jnp.asarray([[1.]],dtype=jnp.float32)
for _ in range(n_trials):
spikes = []
reset_process.run()
for ts in range(T):
clamp(probs)
advance_process.run(t=ts * 1., dt=dt)
s_t = cell.outputs.get()
spikes.append(s_t)
count = jnp.sum(jnp.concatenate(spikes, axis=0))
mu += count
print("Mean firing rate = {} Hertz".format(mu/n_trials))
```
which should print to I/O the following:
```console
Mean firing rate = 63.833336 Hertz
```
You now have two very useful input encoding cells to convert real-valued
data to spike trains. Note that both the Bernoulli and Poisson cell assume
that the each feature in your input sensory pattern vectors lie in the range of
`[0,1]`, so it is important to make sure that your data's values conform to
this assumption (e.g., divide the pixel values in MNIST by `255`).