CARLsim  4.1.0
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)
CARLsim::createSpikeGeneratorGroup
int createSpikeGeneratorGroup(const std::string &grpName, int nNeur, int neurType, int preferredPartition=ANY, ComputingBackend preferredBackend=CPU_CORES)
creates a spike generator group
Definition: carlsim.cpp:1779
SimpleWeightTuner
Class SimpleWeightTuner.
Definition: simple_weight_tuner.h:88
RangeWeight
a range struct for synaptic weight magnitudes
Definition: carlsim_datastructures.h:311
SpikeMonitor
Class SpikeMonitor.
Definition: spike_monitor.h:119
CARLsim::runNetwork
int runNetwork(int nSec, int nMsec=0, bool printRunSummary=true)
run the simulation for time=(nSec*seconds + nMsec*milliseconds)
Definition: carlsim.cpp:1909
CARLsim::setConductances
void setConductances(bool isSet)
Sets default values for conduction decay and rise times or disables COBA alltogether.
Definition: carlsim.cpp:1788
EXCITATORY_NEURON
#define EXCITATORY_NEURON
Definition: carlsim_definitions.h:76
USER
@ USER
User mode, for experiment-oriented simulations.
Definition: carlsim_datastructures.h:91
CARLsim::setSpikeGenerator
void setSpikeGenerator(int grpId, SpikeGenerator *spikeGenFunc)
A SpikeCounter keeps track of the number of spikes per neuron in a group.
Definition: carlsim.cpp:1967
CARLsim::setupNetwork
void setupNetwork()
build the network
Definition: carlsim.cpp:1914
RangeDelay
a range struct for synaptic delays
Definition: carlsim_datastructures.h:278
CARLsim::setNeuronParameters
void setNeuronParameters(int grpId, float izh_a, float izh_a_sd, float izh_b, float izh_b_sd, float izh_c, float izh_c_sd, float izh_d, float izh_d_sd)
Sets Izhikevich params a, b, c, and d with as mean +- standard deviation.
Definition: carlsim.cpp:1815
CPU_MODE
@ CPU_MODE
model is run on CPU core(s)
Definition: carlsim_datastructures.h:114
CARLsim
CARLsim User Interface This class provides a user interface to the public sections of CARLsimCore sou...
Definition: carlsim.h:137
CARLsim::connect
short int connect(int grpId1, int grpId2, const std::string &connType, const RangeWeight &wt, float connProb, const RangeDelay &delay=RangeDelay(1), const RadiusRF &radRF=RadiusRF(-1.0), bool synWtType=SYN_FIXED, float mulSynFast=1.0f, float mulSynSlow=1.0f)
Connects a presynaptic to a postsynaptic group using fixed/plastic weights and a range of delay value...
Definition: carlsim.cpp:1739
PeriodicSpikeGenerator
a periodic SpikeGenerator (constant ISI) creating spikes at a certain rate
Definition: periodic_spikegen.h:61
CARLsim::createGroup
int createGroup(const std::string &grpName, int nNeur, int neurType, int preferredPartition=ANY, ComputingBackend preferredBackend=CPU_CORES)
creates a group of Izhikevich spiking neurons
Definition: carlsim.cpp:1763