Lesson 3: Evolving Synaptic Efficacies
In this tutorial, we will extend a model context/controller with three components, two cell components connected with a synaptic cable component, to incorporate a basic a two-factor Hebbian adjustment process.
Adding a Learnable Synapse to a Multi-Component System
Create a Python script/file named run_lesson3.py to place/write your Python code below into.
Let us start by building a controller/model-context similar to previous lessons with the one exception that now we will
trigger the synaptic connection between a and b to adapt via a simple 2-factor Hebbian rule. This Hebbian rule will
require us to wire the output compartment of a to the pre-synaptic compartment of the synapse Wab and the output
compartment of b to the post-synaptic compartment of Wab. This will wire in the two relevant factors needed to
compute a simple Hebbian adjustment.
We do this specifically as follows:
from jax import numpy as jnp, random, jit
from ngclearn import Context, MethodProcess
from ngclearn.components import HebbianSynapse, RateCell
from ngclearn.utils.distribution_generator import DistributionGenerator as dist
## create seeding keys
dkey = random.PRNGKey(1234)
dkey, *subkeys = random.split(dkey, 6)
## create simple system with only one F-N cell
with Context("Circuit") as circuit:
a = RateCell(name="a", n_units=1, tau_m=0., act_fx="identity", key=subkeys[0])
b = RateCell(name="b", n_units=1, tau_m=0., act_fx="identity", key=subkeys[1])
Wab = HebbianSynapse(
name="Wab", shape=(1, 1), eta=1., sign_value=-1., weight_init=dist.constant(value=1.),
w_bound=0., key=subkeys[3]
)
# wire output compartment (rate-coded output zF) of RateCell `a` to input compartment of HebbianSynapse `Wab`
a.zF >> Wab.inputs
# wire output compartment of HebbianSynapse `Wab` to input compartment (electrical current j) RateCell `b`
Wab.outputs >> b.j
# wire output compartment (rate-coded output zF) of RateCell `a` to presynaptic compartment of HebbianSynapse `Wab`
a.zF >> Wab.pre
# wire output compartment (rate-coded output zF) of RateCell `b` to postsynaptic compartment of HebbianSynapse `Wab`
b.zF >> Wab.post
## create and compile core simulation commands
evolve = (MethodProcess("evolve")
>> a.evolve)
advance = (MethodProcess("advance")
>> a.advance_state)
reset = (MethodProcess("reset")
>> a.reset)
## set up non-compiled utility commands
def clamp(x):
a.j.set(x)
Now with our simple system above created, we will now run a simple sequence of one-dimensional “spike” data through it and evolve the synapse every time step like so:
## run some data through the dynamical system
x_seq = jnp.asarray([[1, 1, 0, 0, 1]], dtype=jnp.float32)
reset.run()
print("{}: Wab = {}".format(-1, Wab.weights.value))
for ts in range(x_seq.shape[1]):
x_t = jnp.expand_dims(x_seq[0,ts], axis=0) ## get data at time t
clamp(x_t)
advance.run(t=ts*1., dt=1.)
evolve.run(t=ts*1., dt=1.)
print(" {}: input = {} ~> Wab = {}".format(ts, x_t, Wab.weights.get()))
After running run_lesson3.py, your code should produce (printed to I/O) the same output as below:
-1: Wab = [[1.]]
0: input = [1.] ~> Wab = [[2.]]
1: input = [1.] ~> Wab = [[4.]]
2: input = [0.] ~> Wab = [[4.]]
3: input = [0.] ~> Wab = [[4.]]
4: input = [1.] ~> Wab = [[8.]]
Notice that for every non-spike (a value of 0), the synaptic value remains the same (because the product of a
pre-synaptic value of 0 with a post-synaptic value of anything – in this case, also a 0 – is simply 0, meaning
that no change will be applied to the synapse). For every spike (a value of 1), we get a synaptic change equal to
dW = input * (Wab * input); so for the first time-step, the weight will change according to
W = W + eta * dW = W + dW and dW = 1 * (1 * 1) = 1, whereas, for the second time-step, W will be increased by
dW = 1 * (2 * 1) = 2 (yielding a new synaptic strength of W = 4).
As per the above, you have now created your first plastic, evolving neuronal system!