CARLsim  3.1.3
CARLsim: a GPU-accelerated SNN simulator
Tutorial 6: Simple Weight Tuner

Table of Contents

Author
Michael Beyeler
See also
12.4 On-Line Weight Tuning
12.4.1 Simple Weight Tuner

In this tutorial, we will learn how to use the SimpleWeightTuner utility, which is a piece of software that allows us to tune weight values of a specific connection (i.e., a collection of synapses) on-the-fly, so that a specific neuron group fires at a predefined target firing rate.

We will assume a three-layer feedforward network (with a single hidden layer), whose weights we would like to tune so that both hidden layer and output layer fire at some specific (but different) firing rate. In order to achieve this, we will first tune the weights from the input layer to the hidden layer, based on observed hidden layer activity, and then tune the weights from the hidden layer to the output layer, based on observed output layer activity.

This tutorial assumes that you know how to configure a CARLsim network (by setting up groups and connections).

The complete source code explained in this tutorial can be found in doc/source/tutorial/5_simple_weight_tuner and consists of the following files and folders:

  • Makefile: The Makefile used to compile the C++ source file
  • main_simple_weight_tuner.cpp: A C++ source file that implements the network in CARLsim
  • results/: A directory that will contain all metadata and results generated by the simulation
  • ( doc: A directory that contains this Doxygen source code )

In order to compile and run the source code (on Linux), open a terminal, navigate to doc/source/tutorial/5_simple_weight_tuner, and type:

$ make
$ ./simple_weight_tuner

6.1 Problem

Suppose we are given a feedforward network of a certain topology, and we are interested in finding the synaptic weight values that lead to neuronal firing at a specific target firing rate.

The most straightfoward approach is to hand-tune the weights; by starting at some reasonable weight value, observing network's output activity to some stimulus, and then varying the weight value by hand until some target firing rate is reached. Although this approach might lead to a valid solution, it might also be a time-consuming and tedious procedure. An alternative approach would be to perform local search on the domain of valid weight values using the Parameter Tuning Interface (see Chapter 10: ECJ), but specifying a usable fitness function and performing the search might actually take more time than simple hand-tuning.

In this tutorial, we will take an intermediate approach. The SimpleWeightTuner utility automates the procedure of manual weight tuning by repeatedly updating the weights according to some observed output activity. The idea of the utility is pretty simple (inspired by the bisection method): If a given weight leads to an output firing rate below the target firing rate, increase the weight by a certain step size. As soon as the chosen weight leads to activity above some target firing rate, change the sign of the weight and cut its magnitude in half. Repeat until activity is within some error margin of the target firing rate, or a maximum number of iteration steps was reached.

6.2 Solution

Consider the following three-layer feedforward network:

// ---------------- CONFIG STATE -------------------
CARLsim *sim = new CARLsim("SimpleWeightTuner", CPU_MODE, USER, 0, 42);
// output layer should have some target firing rate
int gOut=sim->createGroup("out", 1000, EXCITATORY_NEURON);
sim->setNeuronParameters(gOut, 0.02f, 0.2f, -66.0f, 8.0f);
// hidden layer to tune first
int gHid=sim->createGroup("hidden", 1000, EXCITATORY_NEURON);
sim->setNeuronParameters(gHid, 0.02f, 0.2f, -66.0f, 8.0f);
// input is a SpikeGenerator group that fires every 20 ms (50 Hz)
int gIn=sim->createSpikeGeneratorGroup("in", 1000, EXCITATORY_NEURON);
sim->setSpikeGenerator(gIn, &PSG);
// random connection with 10% probability
int c0=sim->connect(gIn, gHid, "random", RangeWeight(0.005f), 0.1f, RangeDelay(1,10));
int c1=sim->connect(gHid, gOut, "random", RangeWeight(0.005f), 0.1f, RangeDelay(1,10));
sim->setConductances(true);

Instead of hand-tuning the weights for c0 and c1 until groups gHid and gOut fire at some desired firing rate, we will automate the tuning using SimpleWeightTuner:

// ---------------- SETUP STATE -------------------
sim->setupNetwork();
// accept firing rates within this range of target firing
double targetFiringHid = 27.4; // target firing rate for gHid
double targetFiringOut = 42.8; // target firing rate for gOut
// algorithm will terminate when at least one of the termination conditions is reached
double errorMarginHz = 0.015; // error margin
int maxIter = 100; // max number of iterations
// set up weight tuning from input -> hidden
SimpleWeightTuner SWTin2hid(sim, errorMarginHz, maxIter);
SWTin2hid.setConnectionToTune(c0, 0.0); // start at 0
SWTin2hid.setTargetFiringRate(gHid, targetFiringHid);
// set up weight tuning from hidden -> output
SimpleWeightTuner SWThid2out(sim, errorMarginHz, maxIter);
SWThid2out.setConnectionToTune(c1, 0.0); // start at 0
SWThid2out.setTargetFiringRate(gOut, targetFiringOut);

Here we define that gHid should fire at 27.4Hz, and gOut should fire at 42.8Hz, with an error margin of +- 0.015Hz (arbitrary values).

We create two instances of class SimpleWeightTuner and pass to it a pointer to the above created CARLsim network (sim) as well as the termination conditions: The algorithm will terminate either if the absolute error between observed firing rate and target firing rate is smaller than some error margin (errorMarginHz), or upon reaching the maximum number of iterations (maxIter).

The goal is to first tune the weights of connection ID c1, which are the weights connecting the input layer gIn to the hidden layer gHid, until the hidden layer fires at 27.4Hz, and then to tune the weights of connection ID c2, which are the weights connecting the hidden layer to the ouput layer gOut, until the ouput layer fires at 42.8Hz. For this, we pass the connection ID and initial weight to SimpleWeightTuner::setConnectionToTune. SimpleWeightTuner::setTargetFiringRate then specifies which group should fire at which rate. Note that the connection and group specified here are completely independent from each other.

All that is left to do is to execute the algorithm until finished:

// ---------------- RUN STATE -------------------
printf("\nSimpleWeightTuner Demo\n");
printf("- Step 1: Tune weights from input layer to hidden layer\n");
while (!SWTin2hid.done()) {
SWTin2hid.iterate();
}
printf("\n- Step 2: Tune weights from hidden layer to output layer\n");
while (!SWThid2out.done()) {
SWThid2out.iterate();
}

Each while loop will repeatedly run the CARLsim network for a short amount of time (default: 1s), during which spikes of the group specified in SimpleWeightTuner::setTargetFiringRate are recorded. At the end of each iteration, the observed firing rate is compared to the target firing rate, and weights are updated accordingly (using CARLsim::biasWeights). This procedure is repeated until one of the termination condition is reached (indicated by SimpleWeightTuner::done returning value true), at which point we are done.

We could now retrieve the tuned weights using a ConnectionMonitor or save the entire network state to file via CARLsim::saveSimulation so that it can later be reloaded using CARLsim::loadSimulation. Alternatively, we could just run the network a bit more to convince ourselves that the groups fire at the correct firing rate:

printf("\n- Step 3: Verify result (gHid=%.4fHz, gOut=%.4fHz, +/- %.4fHz)\n", targetFiringHid, targetFiringOut,
errorMarginHz);
sim->runNetwork(1,0);

The complete code produces the following console output:

- Step 1: Tune weights from input layer to hidden layer
#0: rate=0.0000Hz, target=27.4000Hz, error=-27.4000000, errorMargin=0.0100000
#1: rate=26.4430Hz, target=27.4000Hz, error=-0.9569992, errorMargin=0.0100000
#2: rate=51.7540Hz, target=27.4000Hz, error=24.3540016, errorMargin=0.0100000
#3: rate=40.6780Hz, target=27.4000Hz, error=13.2780014, errorMargin=0.0100000
#4: rate=27.1590Hz, target=27.4000Hz, error=-0.2409996, errorMargin=0.0100000
#5: rate=33.2920Hz, target=27.4000Hz, error=6.8919998, errorMargin=0.0100000
#8: rate=28.5820Hz, target=27.4000Hz, error=1.1820007, errorMargin=0.0100000
#9: rate=27.7840Hz, target=27.4000Hz, error=0.3840004, errorMargin=0.0100000
#10: rate=26.9060Hz, target=27.4000Hz, error=-0.4939999, errorMargin=0.0100000
#11: rate=27.3400Hz, target=27.4000Hz, error=-0.0599998, errorMargin=0.0100000
#12: rate=27.7650Hz, target=27.4000Hz, error=0.3649994, errorMargin=0.0100000
#13: rate=27.5420Hz, target=27.4000Hz, error=0.1419998, errorMargin=0.0100000
#14: rate=27.3450Hz, target=27.4000Hz, error=-0.0550007, errorMargin=0.0100000
#15: rate=27.4410Hz, target=27.4000Hz, error=0.0410000, errorMargin=0.0100000
#16: rate=27.3840Hz, target=27.4000Hz, error=-0.0159992, errorMargin=0.0100000
#17: rate=27.4070Hz, target=27.4000Hz, error=0.0069996, errorMargin=0.0100000
SimpleWeightTuner successful: Error margin reached in 18 iterations.
- Step 2: Tune weights from hidden layer to output layer
#0: rate=1.1540Hz, target=42.8000Hz, error=-41.6460000, errorMargin=0.0100000
#1: rate=16.4940Hz, target=42.8000Hz, error=-27.3059996, errorMargin=0.0100000
#2: rate=28.0930Hz, target=42.8000Hz, error=-14.7069996, errorMargin=0.0100000
#3: rate=42.7080Hz, target=42.8000Hz, error=-0.0919998, errorMargin=0.0100000
#4: rate=54.4490Hz, target=42.8000Hz, error=11.6490013, errorMargin=0.0100000
#5: rate=49.4270Hz, target=42.8000Hz, error=6.6269981, errorMargin=0.0100000
#6: rate=43.5590Hz, target=42.8000Hz, error=0.7589981, errorMargin=0.0100000
#7: rate=36.3550Hz, target=42.8000Hz, error=-6.4450005, errorMargin=0.0100000
#8: rate=39.5930Hz, target=42.8000Hz, error=-3.2070015, errorMargin=0.0100000
#9: rate=43.1410Hz, target=42.8000Hz, error=0.3409988, errorMargin=0.0100000
#10: rate=41.6410Hz, target=42.8000Hz, error=-1.1590012, errorMargin=0.0100000
#11: rate=42.3660Hz, target=42.8000Hz, error=-0.4339989, errorMargin=0.0100000
#12: rate=43.2860Hz, target=42.8000Hz, error=0.4859993, errorMargin=0.0100000
#13: rate=42.9020Hz, target=42.8000Hz, error=0.1020004, errorMargin=0.0100000
#14: rate=42.4910Hz, target=42.8000Hz, error=-0.3089989, errorMargin=0.0100000
#15: rate=42.6160Hz, target=42.8000Hz, error=-0.1839989, errorMargin=0.0100000
#16: rate=42.8750Hz, target=42.8000Hz, error=0.0750000, errorMargin=0.0100000
#17: rate=42.7680Hz, target=42.8000Hz, error=-0.0319984, errorMargin=0.0100000
#18: rate=42.8310Hz, target=42.8000Hz, error=0.0310013, errorMargin=0.0100000
#19: rate=42.7700Hz, target=42.8000Hz, error=-0.0299995, errorMargin=0.0100000
#20: rate=42.7790Hz, target=42.8000Hz, error=-0.0210007, errorMargin=0.0100000
#21: rate=42.8660Hz, target=42.8000Hz, error=0.0660011, errorMargin=0.0100000
#22: rate=42.8190Hz, target=42.8000Hz, error=0.0190002, errorMargin=0.0100000
#23: rate=42.7750Hz, target=42.8000Hz, error=-0.0249985, errorMargin=0.0100000
#24: rate=42.7970Hz, target=42.8000Hz, error=-0.0029991, errorMargin=0.0100000
SimpleWeightTuner successful: Error margin reached in 25 iterations.
- Step 3: Verify result (gHid=27.4000Hz, gOut=42.8000Hz, +/- 0.0150Hz)
(t=44.000s) SpikeMonitor for group out(0) has 42783 spikes in 1000ms (42.78 +/- 4.17 Hz)
(t=44.000s) SpikeMonitor for group hidden(1) has 27412 spikes in 1000ms (27.41 +/- 2.72 Hz)