Presenting latency-coded inputs

In this first tutorial we build an input layer of spiking “projection neurons” for our mushroom body model which converts MNIST digits into latency-coded spikes.

Install PyGeNN wheel from Google Drive

Download wheel file

[ ]:
if "google.colab" in str(get_ipython()):
    #import IPython
    #IPython.core.magics.execution.ExecutionMagics.run.func_defaults[2] = lambda a: a
    #%run "../install_collab.ipynb"
    !pip install gdown --upgrade
    !gdown 1V_GzXUDzcFz9QDIpxAD8QNEglcSipssW
    !pip install pygenn-5.0.0-cp310-cp310-linux_x86_64.whl
    %env CUDA_PATH=/usr/local/cuda
Requirement already satisfied: gdown in /usr/local/lib/python3.10/dist-packages (5.1.0)
Requirement already satisfied: beautifulsoup4 in /usr/local/lib/python3.10/dist-packages (from gdown) (4.12.3)
Requirement already satisfied: filelock in /usr/local/lib/python3.10/dist-packages (from gdown) (3.13.1)
Requirement already satisfied: requests[socks] in /usr/local/lib/python3.10/dist-packages (from gdown) (2.31.0)
Requirement already satisfied: tqdm in /usr/local/lib/python3.10/dist-packages (from gdown) (4.66.2)
Requirement already satisfied: soupsieve>1.2 in /usr/local/lib/python3.10/dist-packages (from beautifulsoup4->gdown) (2.5)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests[socks]->gdown) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests[socks]->gdown) (3.6)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests[socks]->gdown) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests[socks]->gdown) (2024.2.2)
Requirement already satisfied: PySocks!=1.5.7,>=1.5.6 in /usr/local/lib/python3.10/dist-packages (from requests[socks]->gdown) (1.7.1)
Downloading...
From: https://drive.google.com/uc?id=1V_GzXUDzcFz9QDIpxAD8QNEglcSipssW
To: /content/pygenn-5.0.0-cp310-cp310-linux_x86_64.whl
100% 8.29M/8.29M [00:00<00:00, 279MB/s]
Processing ./pygenn-5.0.0-cp310-cp310-linux_x86_64.whl
Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.10/dist-packages (from pygenn==5.0.0) (1.25.2)
Requirement already satisfied: deprecated in /usr/local/lib/python3.10/dist-packages (from pygenn==5.0.0) (1.2.14)
Requirement already satisfied: psutil in /usr/local/lib/python3.10/dist-packages (from pygenn==5.0.0) (5.9.5)
Requirement already satisfied: wrapt<2,>=1.10 in /usr/local/lib/python3.10/dist-packages (from deprecated->pygenn==5.0.0) (1.14.1)
pygenn is already installed with the same version as the provided wheel. Use --force-reinstall to force an installation of the wheel.
env: CUDA_PATH=/usr/local/cuda

Install MNIST package

[ ]:
!pip install mnist
Collecting mnist
  Downloading mnist-0.2.2-py2.py3-none-any.whl (3.5 kB)
Requirement already satisfied: numpy in /usr/local/lib/python3.10/dist-packages (from mnist) (1.25.2)
Installing collected packages: mnist
Successfully installed mnist-0.2.2

Build tutorial model

Import modules

[ ]:
import mnist
import numpy as np
from copy import copy
from matplotlib import pyplot as plt
from pygenn import create_current_source_model, init_postsynaptic, init_weight_update, GeNNModel

Load training images from downloaded file and normalise so each image’s pixels add up to one

[ ]:
training_images = mnist.train_images()
training_images = np.reshape(training_images, (training_images.shape[0], -1)).astype(np.float32)

training_images /= np.sum(training_images, axis=1)[:, np.newaxis]

Visualize training data

Reshape first training image from 784 element vector to 28x28 matrix and visualize.

[ ]:
fig, axis = plt.subplots()
axis.imshow(np.reshape(training_images[0], (28, 28)));
../../_images/tutorials_mushroom_body_1_first_layer_9_0.png

Parameters

Define some model parameters

[ ]:
# Simulation time step
DT = 0.1

# Scaling factor for converting normalised image pixels to input currents (nA)
INPUT_SCALE = 80.0

# Number of Projection Neurons in model (should match image size)
NUM_PN = 784

# How long to present each image to model
PRESENT_TIME_MS = 20.0

Define a standard set of parameters to use for all leaky-integrate and fire neurons

[ ]:
# Standard LIF neurons parameters
LIF_PARAMS = {
    "C": 0.2,
    "TauM": 20.0,
    "Vrest": -60.0,
    "Vreset": -60.0,
    "Vthresh": -50.0,
    "Ioffset": 0.0,
    "TauRefrac": 2.0}

Make a copy of this to customise for our Projection neurons and increase the refractory time way above PRESENT_TIME_MS so these neurons will only spike once per input.

[ ]:
# We only want PNs to spike once
PN_PARAMS = copy(LIF_PARAMS)
PN_PARAMS["TauRefrac"] = 100.0

Custom models

We are going to apply inputs to our model by treating scaled image pixels as neuronal input currents so here we define a simple model to inject the current specified by a state variable. Like all types of custom model in GeNN, the var_name_types kwarg is used to specify state variable names and types

[ ]:
# Current source model, allowing current to be injected into neuron from variable
cs_model = create_current_source_model(
    "cs_model",
    vars=[("magnitude", "scalar")],
    injection_code="injectCurrent(magnitude);")

Model definition

Create a new model called “mnist_mb_first_layer” with floating point precision and set the simulation timestep to our chosen value

[ ]:
# Create model
model = GeNNModel("float", "mnist_mb_first_layer")
model.dt = DT

Add a population of NUM_PN Projection Neurons, using the built-in LIF model, the parameters we previously chose and initialising the membrane voltage to the reset voltage.

[ ]:
# Create neuron populations
lif_init = {"V": PN_PARAMS["Vreset"], "RefracTime": 0.0}
pn = model.add_neuron_population("pn", NUM_PN, "LIF", PN_PARAMS, lif_init)

# Turn on spike recording
pn.spike_recording_enabled = True

Add a current source to inject current into pn using our newly-defined custom model with the initial magnitude set to zero.

[ ]:
# Create current sources to deliver input to network
pn_input = model.add_current_source("pn_input", cs_model, pn , {}, {"magnitude": 0.0})

Build model

Generate code and load it into PyGeNN allocating a large enough spike recording buffer to cover PRESENT_TIME_MS (after converting from ms to timesteps)

[ ]:
# Concert present time into timesteps
present_timesteps = int(round(PRESENT_TIME_MS / DT))

# Build model and load it
model.build()
model.load(num_recording_timesteps=present_timesteps)

Simulate tutorial model

In order to ensure that the same stimulus causes exactly the same input each time it is presented, we want to reset the model’s state after presenting each stimulus. This function resets neuron state variables selected by the keys of a dictionary to the values specifed in the dictionary values and pushes the new values to the GPU.

[ ]:
def reset_neuron(pop, var_init):
    # Reset variables
    for var_name, var_val in var_init.items():
        pop.vars[var_name].view[:] = var_val

        # Push the new values to GPU
        pop.vars[var_name].push_to_device()

As an initial test of our model, we loop through 4 stimuli and show the Projection Neurons spikes emitted by the model in response.

[ ]:
for s in range(4):
    # Set training image
    pn_input.vars["magnitude"].view[:] = training_images[s] * INPUT_SCALE
    pn_input.vars["magnitude"].push_to_device()

    # Simulate timesteps
    for i in range(present_timesteps):
        model.step_time()

    # Reset neuron state for next stimuli
    reset_neuron(pn, lif_init)

    # Download spikes from GPU
    model.pull_recording_buffers_from_device();

    # Plot PN spikes
    fig, axis = plt.subplots()
    pn_spike_times, pn_spike_ids = pn.spike_recording_data[0]
    axis.scatter(pn_spike_times, pn_spike_ids, s=1)
    axis.set_xlabel("Time [ms]")
../../_images/tutorials_mushroom_body_1_first_layer_29_0.png
../../_images/tutorials_mushroom_body_1_first_layer_29_1.png
../../_images/tutorials_mushroom_body_1_first_layer_29_2.png
../../_images/tutorials_mushroom_body_1_first_layer_29_3.png